751e2d99c86ce3c2753bb75d3897f2603daadd9f
[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 /* XXX: Should rewrite this to accept a notmuch_message_t* instead of
196  * a Xapian:Document and then we could just use
197  * notmuch_message_get_thread_ids instead of duplicating its logic
198  * here. */
199 static void
200 insert_thread_id (GHashTable *thread_ids, Xapian::Document doc)
201 {
202     string value_string;
203     Xapian::TermIterator i;
204     const char *prefix_str = _find_prefix ("thread");
205     char prefix;
206
207     assert (strlen (prefix_str) == 1);
208
209     prefix = *prefix_str;
210
211     i = doc.termlist_begin ();
212     i.skip_to (prefix_str);
213
214     while (1) {
215         if (i == doc.termlist_end ())
216             break;
217         value_string = *i;
218         if (value_string.empty () || value_string[0] != prefix)
219             break;
220         g_hash_table_insert (thread_ids,
221                              strdup (value_string.c_str () + 1), NULL);
222         i++;
223     }
224 }
225
226 notmuch_message_t *
227 notmuch_database_find_message (notmuch_database_t *notmuch,
228                                const char *message_id)
229 {
230     notmuch_private_status_t status;
231     unsigned int doc_id;
232
233     status = find_unique_doc_id (notmuch, "id", message_id, &doc_id);
234
235     if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
236         return NULL;
237
238     return _notmuch_message_create (notmuch, notmuch, doc_id);
239 }
240
241 /* Return one or more thread_ids, (as a GPtrArray of strings), for the
242  * given message based on looking into the database for any messages
243  * referenced in parents, and also for any messages in the database
244  * referencing message_id.
245  *
246  * Caller should free all strings in the array and the array itself,
247  * (g_ptr_array_free) when done. */
248 static GPtrArray *
249 find_thread_ids (notmuch_database_t *notmuch,
250                  GPtrArray *parents,
251                  const char *message_id)
252 {
253     Xapian::PostingIterator child, children_end;
254     Xapian::Document doc;
255     GHashTable *thread_ids;
256     GList *keys, *l;
257     unsigned int i;
258     const char *parent_message_id;
259     GPtrArray *result;
260
261     thread_ids = g_hash_table_new_full (g_str_hash, g_str_equal,
262                                         free, NULL);
263
264     find_doc_ids (notmuch, "ref", message_id, &child, &children_end);
265     for ( ; child != children_end; child++) {
266         doc = find_document_for_doc_id (notmuch, *child);
267         insert_thread_id (thread_ids, doc);
268     }
269
270     for (i = 0; i < parents->len; i++) {
271         notmuch_message_t *parent;
272         notmuch_thread_ids_t *ids;
273
274         parent_message_id = (char *) g_ptr_array_index (parents, i);
275         parent = notmuch_database_find_message (notmuch, parent_message_id);
276         if (parent == NULL)
277             continue;
278
279         for (ids = notmuch_message_get_thread_ids (parent);
280              notmuch_thread_ids_has_more (ids);
281              notmuch_thread_ids_advance (ids))
282         {
283             const char *id;
284
285             id = notmuch_thread_ids_get (ids);
286             g_hash_table_insert (thread_ids, strdup (id), NULL);
287         }
288
289         notmuch_message_destroy (parent);
290     }
291
292     result = g_ptr_array_new ();
293
294     keys = g_hash_table_get_keys (thread_ids);
295     for (l = keys; l; l = l->next) {
296         char *id = (char *) l->data;
297         g_ptr_array_add (result, id);
298     }
299     g_list_free (keys);
300
301     /* We're done with the hash table, but we've taken the pointers to
302      * the allocated strings and put them into our result array, so
303      * tell the hash not to free them on its way out. */
304     g_hash_table_steal_all (thread_ids);
305     g_hash_table_unref (thread_ids);
306
307     return result;
308 }
309
310 /* Advance 'str' past any whitespace or RFC 822 comments. A comment is
311  * a (potentially nested) parenthesized sequence with '\' used to
312  * escape any character (including parentheses).
313  *
314  * If the sequence to be skipped continues to the end of the string,
315  * then 'str' will be left pointing at the final terminating '\0'
316  * character.
317  */
318 static void
319 skip_space_and_comments (const char **str)
320 {
321     const char *s;
322
323     s = *str;
324     while (*s && (isspace (*s) || *s == '(')) {
325         while (*s && isspace (*s))
326             s++;
327         if (*s == '(') {
328             int nesting = 1;
329             s++;
330             while (*s && nesting) {
331                 if (*s == '(')
332                     nesting++;
333                 else if (*s == ')')
334                     nesting--;
335                 else if (*s == '\\')
336                     if (*(s+1))
337                         s++;
338                 s++;
339             }
340         }
341     }
342
343     *str = s;
344 }
345
346 /* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
347  * comments, and the '<' and '>' delimeters.
348  *
349  * If not NULL, then *next will be made to point to the first character
350  * not parsed, (possibly pointing to the final '\0' terminator.
351  *
352  * Returns a newly allocated string which the caller should free()
353  * when done with it.
354  *
355  * Returns NULL if there is any error parsing the message-id. */
356 static char *
357 parse_message_id (const char *message_id, const char **next)
358 {
359     const char *s, *end;
360     char *result;
361
362     if (message_id == NULL)
363         return NULL;
364
365     s = message_id;
366
367     skip_space_and_comments (&s);
368
369     /* Skip any unstructured text as well. */
370     while (*s && *s != '<')
371         s++;
372
373     if (*s == '<') {
374         s++;
375     } else {
376         if (next)
377             *next = s;
378         return NULL;
379     }
380
381     skip_space_and_comments (&s);
382
383     end = s;
384     while (*end && *end != '>')
385         end++;
386     if (next) {
387         if (*end)
388             *next = end + 1;
389         else
390             *next = end;
391     }
392
393     if (end > s && *end == '>')
394         end--;
395     if (end <= s)
396         return NULL;
397
398     result = strndup (s, end - s + 1);
399
400     /* Finally, collapse any whitespace that is within the message-id
401      * itself. */
402     {
403         char *r;
404         int len;
405
406         for (r = result, len = strlen (r); *r; r++, len--)
407             if (*r == ' ' || *r == '\t')
408                 memmove (r, r+1, len);
409     }
410
411     return result;
412 }
413
414 /* Parse a References header value, putting a copy of each referenced
415  * message-id into 'array'. */
416 static void
417 parse_references (GPtrArray *array,
418                   const char *refs)
419 {
420     char *ref;
421
422     if (refs == NULL)
423         return;
424
425     while (*refs) {
426         ref = parse_message_id (refs, &refs);
427
428         if (ref)
429             g_ptr_array_add (array, ref);
430     }
431 }
432
433 char *
434 notmuch_database_default_path (void)
435 {
436     if (getenv ("NOTMUCH_BASE"))
437         return strdup (getenv ("NOTMUCH_BASE"));
438
439     return g_strdup_printf ("%s/mail", getenv ("HOME"));
440 }
441
442 notmuch_database_t *
443 notmuch_database_create (const char *path)
444 {
445     notmuch_database_t *notmuch = NULL;
446     char *notmuch_path = NULL;
447     struct stat st;
448     int err;
449     char *local_path = NULL;
450
451     if (path == NULL)
452         path = local_path = notmuch_database_default_path ();
453
454     err = stat (path, &st);
455     if (err) {
456         fprintf (stderr, "Error: Cannot create database at %s: %s.\n",
457                  path, strerror (errno));
458         goto DONE;
459     }
460
461     if (! S_ISDIR (st.st_mode)) {
462         fprintf (stderr, "Error: Cannot create database at %s: Not a directory.\n",
463                  path);
464         goto DONE;
465     }
466
467     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
468
469     err = mkdir (notmuch_path, 0755);
470
471     if (err) {
472         fprintf (stderr, "Error: Cannot create directory %s: %s.\n",
473                  notmuch_path, strerror (errno));
474         goto DONE;
475     }
476
477     notmuch = notmuch_database_open (path);
478
479   DONE:
480     if (notmuch_path)
481         free (notmuch_path);
482     if (local_path)
483         free (local_path);
484
485     return notmuch;
486 }
487
488 notmuch_database_t *
489 notmuch_database_open (const char *path)
490 {
491     notmuch_database_t *notmuch = NULL;
492     char *notmuch_path = NULL, *xapian_path = NULL;
493     struct stat st;
494     int err;
495     char *local_path = NULL;
496     unsigned int i;
497
498     if (path == NULL)
499         path = local_path = notmuch_database_default_path ();
500
501     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
502
503     err = stat (notmuch_path, &st);
504     if (err) {
505         fprintf (stderr, "Error opening database at %s: %s\n",
506                  notmuch_path, strerror (errno));
507         goto DONE;
508     }
509
510     xapian_path = g_strdup_printf ("%s/%s", notmuch_path, "xapian");
511
512     notmuch = talloc (NULL, notmuch_database_t);
513     notmuch->path = talloc_strdup (notmuch, path);
514
515     try {
516         notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
517                                                            Xapian::DB_CREATE_OR_OPEN);
518         notmuch->query_parser = new Xapian::QueryParser;
519         notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
520         notmuch->query_parser->set_database (*notmuch->xapian_db);
521
522         for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) {
523             prefix_t *prefix = &BOOLEAN_PREFIX_EXTERNAL[i];
524             notmuch->query_parser->add_boolean_prefix (prefix->name,
525                                                        prefix->prefix);
526         }
527     } catch (const Xapian::Error &error) {
528         fprintf (stderr, "A Xapian exception occurred: %s\n",
529                  error.get_msg().c_str());
530     }
531     
532   DONE:
533     if (local_path)
534         free (local_path);
535     if (notmuch_path)
536         free (notmuch_path);
537     if (xapian_path)
538         free (xapian_path);
539
540     return notmuch;
541 }
542
543 void
544 notmuch_database_close (notmuch_database_t *notmuch)
545 {
546     delete notmuch->query_parser;
547     delete notmuch->xapian_db;
548     talloc_free (notmuch);
549 }
550
551 const char *
552 notmuch_database_get_path (notmuch_database_t *notmuch)
553 {
554     return notmuch->path;
555 }
556
557 notmuch_private_status_t
558 find_timestamp_document (notmuch_database_t *notmuch, const char *db_key,
559                          Xapian::Document *doc, unsigned int *doc_id)
560 {
561     return find_unique_document (notmuch, "timestamp", db_key, doc, doc_id);
562 }
563
564 /* We allow the user to use arbitrarily long keys for timestamps,
565  * (they're for filesystem paths after all, which have no limit we
566  * know about). But we have a term-length limit. So if we exceed that,
567  * we'll use the SHA-1 of the user's key as the actual key for
568  * constructing a database term.
569  *
570  * Caution: This function returns a newly allocated string which the
571  * caller should free() when finished.
572  */
573 static char *
574 timestamp_db_key (const char *key)
575 {
576     int term_len = strlen (_find_prefix ("timestamp")) + strlen (key);
577
578     if (term_len > NOTMUCH_TERM_MAX)
579         return notmuch_sha1_of_string (key);
580     else
581         return strdup (key);
582 }
583
584 notmuch_status_t
585 notmuch_database_set_timestamp (notmuch_database_t *notmuch,
586                                 const char *key, time_t timestamp)
587 {
588     Xapian::Document doc;
589     unsigned int doc_id;
590     notmuch_private_status_t status;
591     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
592     char *db_key = NULL;
593
594     db_key = timestamp_db_key (key);
595
596     try {
597         status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
598
599         doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
600                        Xapian::sortable_serialise (timestamp));
601
602         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
603             char *term = talloc_asprintf (NULL, "%s%s",
604                                           _find_prefix ("timestamp"), db_key);
605             doc.add_term (term);
606             talloc_free (term);
607
608             notmuch->xapian_db->add_document (doc);
609         } else {
610             notmuch->xapian_db->replace_document (doc_id, doc);
611         }
612
613     } catch (Xapian::Error &error) {
614         fprintf (stderr, "A Xapian exception occurred: %s.\n",
615                  error.get_msg().c_str());
616         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
617     }
618
619     if (db_key)
620         free (db_key);
621
622     return ret;
623 }
624
625 time_t
626 notmuch_database_get_timestamp (notmuch_database_t *notmuch, const char *key)
627 {
628     Xapian::Document doc;
629     unsigned int doc_id;
630     notmuch_private_status_t status;
631     char *db_key = NULL;
632     time_t ret = 0;
633
634     db_key = timestamp_db_key (key);
635
636     try {
637         status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
638
639         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
640             goto DONE;
641
642         ret =  Xapian::sortable_unserialise (doc.get_value (NOTMUCH_VALUE_TIMESTAMP));
643     } catch (Xapian::Error &error) {
644         goto DONE;
645     }
646
647   DONE:
648     if (db_key)
649         free (db_key);
650
651     return ret;
652 }
653
654 notmuch_status_t
655 notmuch_database_add_message (notmuch_database_t *notmuch,
656                               const char *filename)
657 {
658     notmuch_message_file_t *message_file;
659     notmuch_message_t *message;
660     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
661
662     GPtrArray *parents, *thread_ids;
663
664     const char *refs, *in_reply_to, *date, *header;
665     const char *from, *to, *subject, *old_filename;
666     char *message_id;
667
668     unsigned int i;
669
670     message_file = notmuch_message_file_open (filename);
671     if (message_file == NULL) {
672         ret = NOTMUCH_STATUS_FILE_ERROR;
673         goto DONE;
674     }
675
676     notmuch_message_file_restrict_headers (message_file,
677                                            "date",
678                                            "from",
679                                            "in-reply-to",
680                                            "message-id",
681                                            "references",
682                                            "subject",
683                                            "to",
684                                            (char *) NULL);
685
686     try {
687         /* The first order of business is to find/create a message ID. */
688
689         header = notmuch_message_file_get_header (message_file, "message-id");
690         if (header) {
691             message_id = parse_message_id (header, NULL);
692             /* So the header value isn't RFC-compliant, but it's
693              * better than no message-id at all. */
694             if (message_id == NULL)
695                 message_id = xstrdup (header);
696         } else {
697             /* No message-id at all, let's generate one by taking a
698              * hash over the file's contents. */
699             char *sha1 = notmuch_sha1_of_file (filename);
700
701             /* If that failed too, something is really wrong. Give up. */
702             if (sha1 == NULL) {
703                 ret = NOTMUCH_STATUS_FILE_ERROR;
704                 goto DONE;
705             }
706
707             message_id = g_strdup_printf ("notmuch-sha1-%s", sha1);
708             free (sha1);
709         }
710
711         /* Now that we have a message ID, we get a message object,
712          * (which may or may not reference an existing document in the
713          * database). */
714
715         /* Use NULL for owner since we want to free this locally. */
716
717         /* XXX: This call can fail by either out-of-memory or an
718          * "impossible" Xapian exception. We should rewrite it to
719          * allow us to propagate the error status. */
720         message = _notmuch_message_create_for_message_id (NULL, notmuch,
721                                                           message_id);
722         if (message == NULL) {
723             fprintf (stderr, "Internal error. This shouldn't happen.\n\n");
724             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");
725             exit (1);
726         }
727
728         /* Has a message previously been added with the same ID? */
729         old_filename = notmuch_message_get_filename (message);
730         if (old_filename && strlen (old_filename)) {
731             ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
732             goto DONE;
733         } else {
734             _notmuch_message_set_filename (message, filename);
735             _notmuch_message_add_term (message, "type", "mail");
736         }
737
738         /* Next, find the thread(s) to which this message belongs. */
739         parents = g_ptr_array_new ();
740
741         refs = notmuch_message_file_get_header (message_file, "references");
742         parse_references (parents, refs);
743
744         in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
745         parse_references (parents, in_reply_to);
746
747         for (i = 0; i < parents->len; i++)
748             _notmuch_message_add_term (message, "ref",
749                                        (char *) g_ptr_array_index (parents, i));
750
751         thread_ids = find_thread_ids (notmuch, parents, message_id);
752
753         free (message_id);
754
755         for (i = 0; i < parents->len; i++)
756             g_free (g_ptr_array_index (parents, i));
757         g_ptr_array_free (parents, TRUE);
758
759         if (thread_ids->len) {
760             unsigned int i;
761             GString *thread_id;
762             char *id;
763
764             for (i = 0; i < thread_ids->len; i++) {
765                 id = (char *) thread_ids->pdata[i];
766                 _notmuch_message_add_thread_id (message, id);
767                 if (i == 0)
768                     thread_id = g_string_new (id);
769                 else
770                     g_string_append_printf (thread_id, ",%s", id);
771
772                 free (id);
773             }
774             g_string_free (thread_id, TRUE);
775         } else {
776             _notmuch_message_ensure_thread_id (message);
777         }
778
779         g_ptr_array_free (thread_ids, TRUE);
780
781         date = notmuch_message_file_get_header (message_file, "date");
782         _notmuch_message_set_date (message, date);
783
784         from = notmuch_message_file_get_header (message_file, "from");
785         subject = notmuch_message_file_get_header (message_file, "subject");
786         to = notmuch_message_file_get_header (message_file, "to");
787
788         if (from == NULL &&
789             subject == NULL &&
790             to == NULL)
791         {
792             ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
793             goto DONE;
794         } else {
795             _notmuch_message_sync (message);
796         }
797     } catch (const Xapian::Error &error) {
798         fprintf (stderr, "A Xapian exception occurred: %s.\n",
799                  error.get_msg().c_str());
800         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
801         goto DONE;
802     }
803
804   DONE:
805     if (message)
806         notmuch_message_destroy (message);
807     if (message_file)
808         notmuch_message_file_close (message_file);
809
810     return ret;
811 }