]> git.notmuchmail.org Git - notmuch/blob - lib/message.cc
lib: read reference terms into message struct.
[notmuch] / lib / message.cc
1 /* message.cc - Results of message-based searches from a notmuch database
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 https://www.gnu.org/licenses/ .
17  *
18  * Author: Carl Worth <cworth@cworth.org>
19  */
20
21 #include "notmuch-private.h"
22 #include "database-private.h"
23 #include "message-private.h"
24
25 #include <stdint.h>
26
27 #include <gmime/gmime.h>
28
29 struct _notmuch_message {
30     notmuch_database_t *notmuch;
31     Xapian::docid doc_id;
32     int frozen;
33     char *message_id;
34     char *thread_id;
35     char *in_reply_to;
36     notmuch_string_list_t *tag_list;
37     notmuch_string_list_t *filename_term_list;
38     notmuch_string_list_t *filename_list;
39     char *maildir_flags;
40     char *author;
41     notmuch_message_file_t *message_file;
42     notmuch_string_list_t *property_term_list;
43     notmuch_string_map_t *property_map;
44     notmuch_string_list_t *reference_list;
45     notmuch_message_list_t *replies;
46     unsigned long flags;
47     /* For flags that are initialized on-demand, lazy_flags indicates
48      * if each flag has been initialized. */
49     unsigned long lazy_flags;
50
51     /* Message document modified since last sync */
52     bool modified;
53
54     /* last view of database the struct is synced with */
55     unsigned long last_view;
56
57     Xapian::Document doc;
58     Xapian::termcount termpos;
59 };
60
61 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
62
63 struct maildir_flag_tag {
64     char flag;
65     const char *tag;
66     bool inverse;
67 };
68
69 /* ASCII ordered table of Maildir flags and associated tags */
70 static struct maildir_flag_tag flag2tag[] = {
71     { 'D', "draft",   false},
72     { 'F', "flagged", false},
73     { 'P', "passed",  false},
74     { 'R', "replied", false},
75     { 'S', "unread",  true }
76 };
77
78 /* We end up having to call the destructor explicitly because we had
79  * to use "placement new" in order to initialize C++ objects within a
80  * block that we allocated with talloc. So C++ is making talloc
81  * slightly less simple to use, (we wouldn't need
82  * talloc_set_destructor at all otherwise).
83  */
84 static int
85 _notmuch_message_destructor (notmuch_message_t *message)
86 {
87     message->doc.~Document ();
88
89     return 0;
90 }
91
92 static notmuch_message_t *
93 _notmuch_message_create_for_document (const void *talloc_owner,
94                                       notmuch_database_t *notmuch,
95                                       unsigned int doc_id,
96                                       Xapian::Document doc,
97                                       notmuch_private_status_t *status)
98 {
99     notmuch_message_t *message;
100
101     if (status)
102         *status = NOTMUCH_PRIVATE_STATUS_SUCCESS;
103
104     message = talloc (talloc_owner, notmuch_message_t);
105     if (unlikely (message == NULL)) {
106         if (status)
107             *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
108         return NULL;
109     }
110
111     message->notmuch = notmuch;
112     message->doc_id = doc_id;
113
114     message->frozen = 0;
115     message->flags = 0;
116     message->lazy_flags = 0;
117
118     /* the message is initially not synchronized with Xapian */
119     message->last_view = 0;
120
121     /* Each of these will be lazily created as needed. */
122     message->message_id = NULL;
123     message->thread_id = NULL;
124     message->in_reply_to = NULL;
125     message->tag_list = NULL;
126     message->filename_term_list = NULL;
127     message->filename_list = NULL;
128     message->maildir_flags = NULL;
129     message->message_file = NULL;
130     message->author = NULL;
131     message->property_term_list = NULL;
132     message->property_map = NULL;
133     message->reference_list = NULL;
134
135     message->replies = _notmuch_message_list_create (message);
136     if (unlikely (message->replies == NULL)) {
137         if (status)
138             *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
139         return NULL;
140     }
141
142     /* This is C++'s creepy "placement new", which is really just an
143      * ugly way to call a constructor for a pre-allocated object. So
144      * it's really not an error to not be checking for OUT_OF_MEMORY
145      * here, since this "new" isn't actually allocating memory. This
146      * is language-design comedy of the wrong kind. */
147
148     new (&message->doc) Xapian::Document;
149
150     talloc_set_destructor (message, _notmuch_message_destructor);
151
152     message->doc = doc;
153     message->termpos = 0;
154
155     return message;
156 }
157
158 /* Create a new notmuch_message_t object for an existing document in
159  * the database.
160  *
161  * Here, 'talloc owner' is an optional talloc context to which the new
162  * message will belong. This allows for the caller to not bother
163  * calling notmuch_message_destroy on the message, and know that all
164  * memory will be reclaimed when 'talloc_owner' is freed. The caller
165  * still can call notmuch_message_destroy when finished with the
166  * message if desired.
167  *
168  * The 'talloc_owner' argument can also be NULL, in which case the
169  * caller *is* responsible for calling notmuch_message_destroy.
170  *
171  * If no document exists in the database with document ID of 'doc_id'
172  * then this function returns NULL and optionally sets *status to
173  * NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND.
174  *
175  * This function can also fail to due lack of available memory,
176  * returning NULL and optionally setting *status to
177  * NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY.
178  *
179  * The caller can pass NULL for status if uninterested in
180  * distinguishing these two cases.
181  */
182 notmuch_message_t *
183 _notmuch_message_create (const void *talloc_owner,
184                          notmuch_database_t *notmuch,
185                          unsigned int doc_id,
186                          notmuch_private_status_t *status)
187 {
188     Xapian::Document doc;
189
190     try {
191         doc = notmuch->xapian_db->get_document (doc_id);
192     } catch (const Xapian::DocNotFoundError &error) {
193         if (status)
194             *status = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
195         return NULL;
196     }
197
198     return _notmuch_message_create_for_document (talloc_owner, notmuch,
199                                                  doc_id, doc, status);
200 }
201
202 /* Create a new notmuch_message_t object for a specific message ID,
203  * (which may or may not already exist in the database).
204  *
205  * The 'notmuch' database will be the talloc owner of the returned
206  * message.
207  *
208  * This function returns a valid notmuch_message_t whether or not
209  * there is already a document in the database with the given message
210  * ID. These two cases can be distinguished by the value of *status:
211  *
212  *
213  *   NOTMUCH_PRIVATE_STATUS_SUCCESS:
214  *
215  *     There is already a document with message ID 'message_id' in the
216  *     database. The returned message can be used to query/modify the
217  *     document. The message may be a ghost message.
218  *
219  *   NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND:
220  *
221  *     No document with 'message_id' exists in the database. The
222  *     returned message contains a newly created document (not yet
223  *     added to the database) and a document ID that is known not to
224  *     exist in the database.  This message is "blank"; that is, it
225  *     contains only a message ID and no other metadata. The caller
226  *     can modify the message, and a call to _notmuch_message_sync
227  *     will add the document to the database.
228  *
229  * If an error occurs, this function will return NULL and *status
230  * will be set as appropriate. (The status pointer argument must
231  * not be NULL.)
232  */
233 notmuch_message_t *
234 _notmuch_message_create_for_message_id (notmuch_database_t *notmuch,
235                                         const char *message_id,
236                                         notmuch_private_status_t *status_ret)
237 {
238     notmuch_message_t *message;
239     Xapian::Document doc;
240     unsigned int doc_id;
241     char *term;
242
243     *status_ret = (notmuch_private_status_t) notmuch_database_find_message (notmuch,
244                                                                             message_id,
245                                                                             &message);
246     if (message)
247         return talloc_steal (notmuch, message);
248     else if (*status_ret)
249         return NULL;
250
251     /* If the message ID is too long, substitute its sha1 instead. */
252     if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX)
253         message_id = _notmuch_message_id_compressed (message, message_id);
254
255     term = talloc_asprintf (NULL, "%s%s",
256                             _find_prefix ("id"), message_id);
257     if (term == NULL) {
258         *status_ret = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
259         return NULL;
260     }
261
262     if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
263         INTERNAL_ERROR ("Failure to ensure database is writable.");
264
265     try {
266         doc.add_term (term, 0);
267         talloc_free (term);
268
269         doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id);
270
271         doc_id = _notmuch_database_generate_doc_id (notmuch);
272     } catch (const Xapian::Error &error) {
273         _notmuch_database_log(notmuch_message_get_database (message), "A Xapian exception occurred creating message: %s\n",
274                  error.get_msg().c_str());
275         notmuch->exception_reported = true;
276         *status_ret = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
277         return NULL;
278     }
279
280     message = _notmuch_message_create_for_document (notmuch, notmuch,
281                                                     doc_id, doc, status_ret);
282
283     /* We want to inform the caller that we had to create a new
284      * document. */
285     if (*status_ret == NOTMUCH_PRIVATE_STATUS_SUCCESS)
286         *status_ret = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
287
288     return message;
289 }
290
291 static char *
292 _notmuch_message_get_term (notmuch_message_t *message,
293                            Xapian::TermIterator &i, Xapian::TermIterator &end,
294                            const char *prefix)
295 {
296     int prefix_len = strlen (prefix);
297     char *value;
298
299     i.skip_to (prefix);
300
301     if (i == end)
302         return NULL;
303
304     const std::string &term = *i;
305     if (strncmp (term.c_str(), prefix, prefix_len))
306         return NULL;
307
308     value = talloc_strdup (message, term.c_str() + prefix_len);
309
310 #if DEBUG_DATABASE_SANITY
311     i++;
312
313     if (i != end && strncmp ((*i).c_str (), prefix, prefix_len) == 0) {
314         INTERNAL_ERROR ("Mail (doc_id: %d) has duplicate %s terms: %s and %s\n",
315                         message->doc_id, prefix, value,
316                         (*i).c_str () + prefix_len);
317     }
318 #endif
319
320     return value;
321 }
322
323 /*
324  * For special applications where we only want the thread id, reading
325  * in all metadata is a heavy I/O penalty.
326  */
327 const char *
328 _notmuch_message_get_thread_id_only (notmuch_message_t *message)
329 {
330
331     Xapian::TermIterator i = message->doc.termlist_begin ();
332     Xapian::TermIterator end = message->doc.termlist_end ();
333
334     message->thread_id = _notmuch_message_get_term (message, i, end,
335                                                     _find_prefix ("thread"));
336     return message->thread_id;
337 }
338
339
340 static void
341 _notmuch_message_ensure_metadata (notmuch_message_t *message, void *field)
342 {
343     Xapian::TermIterator i, end;
344
345     if (field && (message->last_view >= message->notmuch->view))
346         return;
347
348     const char *thread_prefix = _find_prefix ("thread"),
349         *tag_prefix = _find_prefix ("tag"),
350         *id_prefix = _find_prefix ("id"),
351         *type_prefix = _find_prefix ("type"),
352         *filename_prefix = _find_prefix ("file-direntry"),
353         *property_prefix = _find_prefix ("property"),
354         *reference_prefix = _find_prefix ("reference"),
355         *replyto_prefix = _find_prefix ("replyto");
356
357     /* We do this all in a single pass because Xapian decompresses the
358      * term list every time you iterate over it.  Thus, while this is
359      * slightly more costly than looking up individual fields if only
360      * one field of the message object is actually used, it's a huge
361      * win as more fields are used. */
362     for (int count=0; count < 3; count++) {
363         try {
364             i = message->doc.termlist_begin ();
365             end = message->doc.termlist_end ();
366
367             /* Get thread */
368             if (!message->thread_id)
369                 message->thread_id =
370                     _notmuch_message_get_term (message, i, end, thread_prefix);
371
372             /* Get tags */
373             assert (strcmp (thread_prefix, tag_prefix) < 0);
374             if (!message->tag_list) {
375                 message->tag_list =
376                     _notmuch_database_get_terms_with_prefix (message, i, end,
377                                                              tag_prefix);
378                 _notmuch_string_list_sort (message->tag_list);
379             }
380
381             /* Get id */
382             assert (strcmp (tag_prefix, id_prefix) < 0);
383             if (!message->message_id)
384                 message->message_id =
385                     _notmuch_message_get_term (message, i, end, id_prefix);
386
387             /* Get document type */
388             assert (strcmp (id_prefix, type_prefix) < 0);
389             if (! NOTMUCH_TEST_BIT (message->lazy_flags, NOTMUCH_MESSAGE_FLAG_GHOST)) {
390                 i.skip_to (type_prefix);
391                 /* "T" is the prefix "type" fields.  See
392                  * BOOLEAN_PREFIX_INTERNAL. */
393                 if (*i == "Tmail")
394                     NOTMUCH_CLEAR_BIT (&message->flags, NOTMUCH_MESSAGE_FLAG_GHOST);
395                 else if (*i == "Tghost")
396                     NOTMUCH_SET_BIT (&message->flags, NOTMUCH_MESSAGE_FLAG_GHOST);
397                 else
398                     INTERNAL_ERROR ("Message without type term");
399                 NOTMUCH_SET_BIT (&message->lazy_flags, NOTMUCH_MESSAGE_FLAG_GHOST);
400             }
401
402             /* Get filename list.  Here we get only the terms.  We lazily
403              * expand them to full file names when needed in
404              * _notmuch_message_ensure_filename_list. */
405             assert (strcmp (type_prefix, filename_prefix) < 0);
406             if (!message->filename_term_list && !message->filename_list)
407                 message->filename_term_list =
408                     _notmuch_database_get_terms_with_prefix (message, i, end,
409                                                              filename_prefix);
410
411
412             /* Get property terms. Mimic the setup with filenames above */
413             assert (strcmp (filename_prefix, property_prefix) < 0);
414             if (!message->property_map && !message->property_term_list)
415                 message->property_term_list =
416                     _notmuch_database_get_terms_with_prefix (message, i, end,
417                                                          property_prefix);
418
419             /* get references */
420             assert (strcmp (property_prefix, reference_prefix) < 0);
421             if (!message->reference_list) {
422                 message->reference_list =
423                     _notmuch_database_get_terms_with_prefix (message, i, end,
424                                                              reference_prefix);
425             }
426
427             /* Get reply to */
428             assert (strcmp (property_prefix, replyto_prefix) < 0);
429             if (!message->in_reply_to)
430                 message->in_reply_to =
431                     _notmuch_message_get_term (message, i, end, replyto_prefix);
432
433
434             /* It's perfectly valid for a message to have no In-Reply-To
435              * header. For these cases, we return an empty string. */
436             if (!message->in_reply_to)
437                 message->in_reply_to = talloc_strdup (message, "");
438
439             /* all the way without an exception */
440             break;
441         } catch (const Xapian::DatabaseModifiedError &error) {
442             notmuch_status_t status = _notmuch_database_reopen (message->notmuch);
443             if (status != NOTMUCH_STATUS_SUCCESS)
444                 INTERNAL_ERROR ("unhandled error from notmuch_database_reopen: %s\n",
445                                 notmuch_status_to_string (status));
446         } catch (const Xapian::Error &error) {
447             INTERNAL_ERROR ("A Xapian exception occurred fetching message metadata: %s\n",
448                             error.get_msg().c_str());
449         }
450     }
451     message->last_view = message->notmuch->view;
452 }
453
454 void
455 _notmuch_message_invalidate_metadata (notmuch_message_t *message,
456                                       const char *prefix_name)
457 {
458     if (strcmp ("thread", prefix_name) == 0) {
459         talloc_free (message->thread_id);
460         message->thread_id = NULL;
461     }
462
463     if (strcmp ("tag", prefix_name) == 0) {
464         talloc_unlink (message, message->tag_list);
465         message->tag_list = NULL;
466     }
467
468     if (strcmp ("type", prefix_name) == 0) {
469         NOTMUCH_CLEAR_BIT (&message->flags, NOTMUCH_MESSAGE_FLAG_GHOST);
470         NOTMUCH_CLEAR_BIT (&message->lazy_flags, NOTMUCH_MESSAGE_FLAG_GHOST);
471     }
472
473     if (strcmp ("file-direntry", prefix_name) == 0) {
474         talloc_free (message->filename_term_list);
475         talloc_free (message->filename_list);
476         message->filename_term_list = message->filename_list = NULL;
477     }
478
479     if (strcmp ("property", prefix_name) == 0) {
480
481         if (message->property_term_list)
482             talloc_free (message->property_term_list);
483         message->property_term_list = NULL;
484
485         if (message->property_map)
486             talloc_unlink (message, message->property_map);
487
488         message->property_map = NULL;
489     }
490
491     if (strcmp ("replyto", prefix_name) == 0) {
492         talloc_free (message->in_reply_to);
493         message->in_reply_to = NULL;
494     }
495 }
496
497 unsigned int
498 _notmuch_message_get_doc_id (notmuch_message_t *message)
499 {
500     return message->doc_id;
501 }
502
503 const char *
504 notmuch_message_get_message_id (notmuch_message_t *message)
505 {
506     _notmuch_message_ensure_metadata (message, message->message_id);
507     if (!message->message_id)
508         INTERNAL_ERROR ("Message with document ID of %u has no message ID.\n",
509                         message->doc_id);
510     return message->message_id;
511 }
512
513 static void
514 _notmuch_message_ensure_message_file (notmuch_message_t *message)
515 {
516     const char *filename;
517
518     if (message->message_file)
519         return;
520
521     filename = notmuch_message_get_filename (message);
522     if (unlikely (filename == NULL))
523         return;
524
525     message->message_file = _notmuch_message_file_open_ctx (
526         notmuch_message_get_database (message), message, filename);
527 }
528
529 const char *
530 notmuch_message_get_header (notmuch_message_t *message, const char *header)
531 {
532     Xapian::valueno slot = Xapian::BAD_VALUENO;
533
534     /* Fetch header from the appropriate xapian value field if
535      * available */
536     if (strcasecmp (header, "from") == 0)
537         slot = NOTMUCH_VALUE_FROM;
538     else if (strcasecmp (header, "subject") == 0)
539         slot = NOTMUCH_VALUE_SUBJECT;
540     else if (strcasecmp (header, "message-id") == 0)
541         slot = NOTMUCH_VALUE_MESSAGE_ID;
542
543     if (slot != Xapian::BAD_VALUENO) {
544         try {
545             std::string value = message->doc.get_value (slot);
546
547             /* If we have NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES, then
548              * empty values indicate empty headers.  If we don't, then
549              * it could just mean we didn't record the header. */
550             if ((message->notmuch->features &
551                  NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES) ||
552                 ! value.empty())
553                 return talloc_strdup (message, value.c_str ());
554
555         } catch (Xapian::Error &error) {
556             _notmuch_database_log(notmuch_message_get_database (message), "A Xapian exception occurred when reading header: %s\n",
557                      error.get_msg().c_str());
558             message->notmuch->exception_reported = true;
559             return NULL;
560         }
561     }
562
563     /* Otherwise fall back to parsing the file */
564     _notmuch_message_ensure_message_file (message);
565     if (message->message_file == NULL)
566         return NULL;
567
568     return _notmuch_message_file_get_header (message->message_file, header);
569 }
570
571 /* Return the message ID from the In-Reply-To header of 'message'.
572  *
573  * Returns an empty string ("") if 'message' has no In-Reply-To
574  * header.
575  *
576  * Returns NULL if any error occurs.
577  */
578 const char *
579 _notmuch_message_get_in_reply_to (notmuch_message_t *message)
580 {
581     _notmuch_message_ensure_metadata (message, message->in_reply_to);
582     return message->in_reply_to;
583 }
584
585 const char *
586 notmuch_message_get_thread_id (notmuch_message_t *message)
587 {
588     _notmuch_message_ensure_metadata (message, message->thread_id);
589     if (!message->thread_id)
590         INTERNAL_ERROR ("Message with document ID of %u has no thread ID.\n",
591                         message->doc_id);
592     return message->thread_id;
593 }
594
595 void
596 _notmuch_message_add_reply (notmuch_message_t *message,
597                             notmuch_message_t *reply)
598 {
599     _notmuch_message_list_add_message (message->replies, reply);
600 }
601
602 const notmuch_string_list_t *
603 _notmuch_message_get_references (notmuch_message_t *message)
604 {
605     _notmuch_message_ensure_metadata (message, message->reference_list);
606     return message->reference_list;
607 }
608
609 static int
610 _cmpmsg (const void *pa, const void *pb)
611 {
612     notmuch_message_t **a = (notmuch_message_t **) pa;
613     notmuch_message_t **b = (notmuch_message_t **) pb;
614     time_t time_a = notmuch_message_get_date (*a);
615     time_t time_b = notmuch_message_get_date (*b);
616
617     if (time_a == time_b)
618         return 0;
619     else if (time_a < time_b)
620         return -1;
621     else
622         return 1;
623 }
624
625 notmuch_message_list_t *
626 _notmuch_message_sort_subtrees (void *ctx, notmuch_message_list_t *list)
627 {
628
629     size_t count = 0;
630     size_t capacity = 16;
631
632     if (! list)
633         return list;
634
635     void *local = talloc_new (NULL);
636     notmuch_message_list_t *new_list = _notmuch_message_list_create (ctx);
637     notmuch_message_t **message_array = talloc_zero_array (local, notmuch_message_t *, capacity);
638
639     for (notmuch_messages_t *messages = _notmuch_messages_create (list);
640          notmuch_messages_valid (messages);
641          notmuch_messages_move_to_next (messages)) {
642         notmuch_message_t *root = notmuch_messages_get (messages);
643         if (count >= capacity) {
644             capacity *= 2;
645             message_array = talloc_realloc (local, message_array, notmuch_message_t *, capacity);
646         }
647         message_array[count++] = root;
648         root->replies = _notmuch_message_sort_subtrees (root, root->replies);
649     }
650
651     qsort (message_array, count, sizeof (notmuch_message_t *), _cmpmsg);
652     for (size_t i = 0; i < count; i++) {
653         _notmuch_message_list_add_message (new_list, message_array[i]);
654     }
655
656     talloc_free (local);
657     talloc_free (list);
658     return new_list;
659 }
660
661 notmuch_messages_t *
662 notmuch_message_get_replies (notmuch_message_t *message)
663 {
664     return _notmuch_messages_create (message->replies);
665 }
666
667 void
668 _notmuch_message_remove_terms (notmuch_message_t *message, const char *prefix)
669 {
670     Xapian::TermIterator i;
671     size_t prefix_len = 0;
672
673     prefix_len = strlen (prefix);
674
675     while (1) {
676         i = message->doc.termlist_begin ();
677         i.skip_to (prefix);
678
679         /* Terminate loop when no terms remain with desired prefix. */
680         if (i == message->doc.termlist_end () ||
681             strncmp ((*i).c_str (), prefix, prefix_len))
682             break;
683
684         try {
685             message->doc.remove_term ((*i));
686             message->modified = true;
687         } catch (const Xapian::InvalidArgumentError) {
688             /* Ignore failure to remove non-existent term. */
689         }
690     }
691 }
692
693
694 /* Remove all terms generated by indexing, i.e. not tags or
695  * properties, along with any automatic tags*/
696 notmuch_private_status_t
697 _notmuch_message_remove_indexed_terms (notmuch_message_t *message)
698 {
699     Xapian::TermIterator i;
700
701     const std::string
702         id_prefix = _find_prefix ("id"),
703         property_prefix = _find_prefix ("property"),
704         tag_prefix = _find_prefix ("tag"),
705         type_prefix = _find_prefix ("type");
706
707     for (i = message->doc.termlist_begin ();
708          i != message->doc.termlist_end (); i++) {
709
710         const std::string term = *i;
711
712         if (term.compare (0, type_prefix.size (), type_prefix) == 0)
713             continue;
714
715         if (term.compare (0, id_prefix.size (), id_prefix) == 0)
716             continue;
717
718         if (term.compare (0, property_prefix.size (), property_prefix) == 0)
719             continue;
720
721         if (term.compare (0, tag_prefix.size (), tag_prefix) == 0 &&
722             term.compare (1, strlen("encrypted"), "encrypted") != 0 &&
723             term.compare (1, strlen("signed"), "signed") != 0 &&
724             term.compare (1, strlen("attachment"), "attachment") != 0)
725             continue;
726
727         try {
728             message->doc.remove_term ((*i));
729             message->modified = true;
730         } catch (const Xapian::InvalidArgumentError) {
731             /* Ignore failure to remove non-existent term. */
732         } catch (const Xapian::Error &error) {
733             notmuch_database_t *notmuch = message->notmuch;
734
735             if (!notmuch->exception_reported) {
736                 _notmuch_database_log(notmuch_message_get_database (message), "A Xapian exception occurred creating message: %s\n",
737                                       error.get_msg().c_str());
738                 notmuch->exception_reported = true;
739             }
740             return NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
741         }
742     }
743     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
744 }
745
746 /* Return true if p points at "new" or "cur". */
747 static bool is_maildir (const char *p)
748 {
749     return strcmp (p, "cur") == 0 || strcmp (p, "new") == 0;
750 }
751
752 /* Add "folder:" term for directory. */
753 static notmuch_status_t
754 _notmuch_message_add_folder_terms (notmuch_message_t *message,
755                                    const char *directory)
756 {
757     char *folder, *last;
758
759     folder = talloc_strdup (NULL, directory);
760     if (! folder)
761         return NOTMUCH_STATUS_OUT_OF_MEMORY;
762
763     /*
764      * If the message file is in a leaf directory named "new" or
765      * "cur", presume maildir and index the parent directory. Thus a
766      * "folder:" prefix search matches messages in the specified
767      * maildir folder, i.e. in the specified directory and its "new"
768      * and "cur" subdirectories.
769      *
770      * Note that this means the "folder:" prefix can't be used for
771      * distinguishing between message files in "new" or "cur". The
772      * "path:" prefix needs to be used for that.
773      *
774      * Note the deliberate difference to _filename_is_in_maildir(). We
775      * don't want to index different things depending on the existence
776      * or non-existence of all maildir sibling directories "new",
777      * "cur", and "tmp". Doing so would be surprising, and difficult
778      * for the user to fix in case all subdirectories were not in
779      * place during indexing.
780      */
781     last = strrchr (folder, '/');
782     if (last) {
783         if (is_maildir (last + 1))
784             *last = '\0';
785     } else if (is_maildir (folder)) {
786         *folder = '\0';
787     }
788
789     _notmuch_message_add_term (message, "folder", folder);
790
791     talloc_free (folder);
792
793     message->modified = true;
794     return NOTMUCH_STATUS_SUCCESS;
795 }
796
797 #define RECURSIVE_SUFFIX "/**"
798
799 /* Add "path:" terms for directory. */
800 static notmuch_status_t
801 _notmuch_message_add_path_terms (notmuch_message_t *message,
802                                  const char *directory)
803 {
804     /* Add exact "path:" term. */
805     _notmuch_message_add_term (message, "path", directory);
806
807     if (strlen (directory)) {
808         char *path, *p;
809
810         path = talloc_asprintf (NULL, "%s%s", directory, RECURSIVE_SUFFIX);
811         if (! path)
812             return NOTMUCH_STATUS_OUT_OF_MEMORY;
813
814         /* Add recursive "path:" terms for directory and all parents. */
815         for (p = path + strlen (path) - 1; p > path; p--) {
816             if (*p == '/') {
817                 strcpy (p, RECURSIVE_SUFFIX);
818                 _notmuch_message_add_term (message, "path", path);
819             }
820         }
821
822         talloc_free (path);
823     }
824
825     /* Recursive all-matching path:** for consistency. */
826     _notmuch_message_add_term (message, "path", "**");
827
828     return NOTMUCH_STATUS_SUCCESS;
829 }
830
831 /* Add directory based terms for all filenames of the message. */
832 static notmuch_status_t
833 _notmuch_message_add_directory_terms (void *ctx, notmuch_message_t *message)
834 {
835     const char *direntry_prefix = _find_prefix ("file-direntry");
836     int direntry_prefix_len = strlen (direntry_prefix);
837     Xapian::TermIterator i = message->doc.termlist_begin ();
838     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
839
840     for (i.skip_to (direntry_prefix); i != message->doc.termlist_end (); i++) {
841         unsigned int directory_id;
842         const char *direntry, *directory;
843         char *colon;
844         const std::string &term = *i;
845
846         /* Terminate loop at first term without desired prefix. */
847         if (strncmp (term.c_str (), direntry_prefix, direntry_prefix_len))
848             break;
849
850         /* Indicate that there are filenames remaining. */
851         status = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
852
853         direntry = term.c_str ();
854         direntry += direntry_prefix_len;
855
856         directory_id = strtol (direntry, &colon, 10);
857
858         if (colon == NULL || *colon != ':')
859             INTERNAL_ERROR ("malformed direntry");
860
861         directory = _notmuch_database_get_directory_path (ctx,
862                                                           message->notmuch,
863                                                           directory_id);
864
865         _notmuch_message_add_folder_terms (message, directory);
866         _notmuch_message_add_path_terms (message, directory);
867     }
868
869     return status;
870 }
871
872 /* Add an additional 'filename' for 'message'.
873  *
874  * This change will not be reflected in the database until the next
875  * call to _notmuch_message_sync. */
876 notmuch_status_t
877 _notmuch_message_add_filename (notmuch_message_t *message,
878                                const char *filename)
879 {
880     const char *relative, *directory;
881     notmuch_status_t status;
882     void *local = talloc_new (message);
883     char *direntry;
884
885     if (filename == NULL)
886         INTERNAL_ERROR ("Message filename cannot be NULL.");
887
888     if (! (message->notmuch->features & NOTMUCH_FEATURE_FILE_TERMS) ||
889         ! (message->notmuch->features & NOTMUCH_FEATURE_BOOL_FOLDER))
890         return NOTMUCH_STATUS_UPGRADE_REQUIRED;
891
892     relative = _notmuch_database_relative_path (message->notmuch, filename);
893
894     status = _notmuch_database_split_path (local, relative, &directory, NULL);
895     if (status)
896         return status;
897
898     status = _notmuch_database_filename_to_direntry (
899         local, message->notmuch, filename, NOTMUCH_FIND_CREATE, &direntry);
900     if (status)
901         return status;
902
903     /* New file-direntry allows navigating to this message with
904      * notmuch_directory_get_child_files() . */
905     _notmuch_message_add_term (message, "file-direntry", direntry);
906
907     _notmuch_message_add_folder_terms (message, directory);
908     _notmuch_message_add_path_terms (message, directory);
909
910     talloc_free (local);
911
912     return NOTMUCH_STATUS_SUCCESS;
913 }
914
915 /* Remove a particular 'filename' from 'message'.
916  *
917  * This change will not be reflected in the database until the next
918  * call to _notmuch_message_sync.
919  *
920  * If this message still has other filenames, returns
921  * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID.
922  *
923  * Note: This function does not remove a document from the database,
924  * even if the specified filename is the only filename for this
925  * message. For that functionality, see
926  * notmuch_database_remove_message. */
927 notmuch_status_t
928 _notmuch_message_remove_filename (notmuch_message_t *message,
929                                   const char *filename)
930 {
931     void *local = talloc_new (message);
932     char *direntry;
933     notmuch_private_status_t private_status;
934     notmuch_status_t status;
935
936     if (! (message->notmuch->features & NOTMUCH_FEATURE_FILE_TERMS) ||
937         ! (message->notmuch->features & NOTMUCH_FEATURE_BOOL_FOLDER))
938         return NOTMUCH_STATUS_UPGRADE_REQUIRED;
939
940     status = _notmuch_database_filename_to_direntry (
941         local, message->notmuch, filename, NOTMUCH_FIND_LOOKUP, &direntry);
942     if (status || !direntry)
943         return status;
944
945     /* Unlink this file from its parent directory. */
946     private_status = _notmuch_message_remove_term (message,
947                                                    "file-direntry", direntry);
948     status = COERCE_STATUS (private_status,
949                             "Unexpected error from _notmuch_message_remove_term");
950     if (status)
951         return status;
952
953     /* Re-synchronize "folder:" and "path:" terms for this message. */
954
955     /* Remove all "folder:" terms. */
956     _notmuch_message_remove_terms (message, _find_prefix ("folder"));
957
958     /* Remove all "path:" terms. */
959     _notmuch_message_remove_terms (message, _find_prefix ("path"));
960
961     /* Add back terms for all remaining filenames of the message. */
962     status = _notmuch_message_add_directory_terms (local, message);
963
964     talloc_free (local);
965
966     return status;
967 }
968
969 /* Upgrade the "folder:" prefix from V1 to V2. */
970 #define FOLDER_PREFIX_V1       "XFOLDER"
971 #define ZFOLDER_PREFIX_V1      "Z" FOLDER_PREFIX_V1
972 void
973 _notmuch_message_upgrade_folder (notmuch_message_t *message)
974 {
975     /* Remove all old "folder:" terms. */
976     _notmuch_message_remove_terms (message, FOLDER_PREFIX_V1);
977
978     /* Remove all old "folder:" stemmed terms. */
979     _notmuch_message_remove_terms (message, ZFOLDER_PREFIX_V1);
980
981     /* Add new boolean "folder:" and "path:" terms. */
982     _notmuch_message_add_directory_terms (message, message);
983 }
984
985 char *
986 _notmuch_message_talloc_copy_data (notmuch_message_t *message)
987 {
988     return talloc_strdup (message, message->doc.get_data ().c_str ());
989 }
990
991 void
992 _notmuch_message_clear_data (notmuch_message_t *message)
993 {
994     message->doc.set_data ("");
995     message->modified = true;
996 }
997
998 static void
999 _notmuch_message_ensure_filename_list (notmuch_message_t *message)
1000 {
1001     notmuch_string_node_t *node;
1002
1003     if (message->filename_list)
1004         return;
1005
1006     _notmuch_message_ensure_metadata (message, message->filename_term_list);
1007
1008     message->filename_list = _notmuch_string_list_create (message);
1009     node = message->filename_term_list->head;
1010
1011     if (!node) {
1012         /* A message document created by an old version of notmuch
1013          * (prior to rename support) will have the filename in the
1014          * data of the document rather than as a file-direntry term.
1015          *
1016          * It would be nice to do the upgrade of the document directly
1017          * here, but the database is likely open in read-only mode. */
1018
1019         std::string datastr = message->doc.get_data ();
1020         const char *data = datastr.c_str ();
1021
1022         if (data == NULL)
1023             INTERNAL_ERROR ("message with no filename");
1024
1025         _notmuch_string_list_append (message->filename_list, data);
1026
1027         return;
1028     }
1029
1030     for (; node; node = node->next) {
1031         void *local = talloc_new (message);
1032         const char *db_path, *directory, *basename, *filename;
1033         char *colon, *direntry = NULL;
1034         unsigned int directory_id;
1035
1036         direntry = node->string;
1037
1038         directory_id = strtol (direntry, &colon, 10);
1039
1040         if (colon == NULL || *colon != ':')
1041             INTERNAL_ERROR ("malformed direntry");
1042
1043         basename = colon + 1;
1044
1045         *colon = '\0';
1046
1047         db_path = notmuch_database_get_path (message->notmuch);
1048
1049         directory = _notmuch_database_get_directory_path (local,
1050                                                           message->notmuch,
1051                                                           directory_id);
1052
1053         if (strlen (directory))
1054             filename = talloc_asprintf (message, "%s/%s/%s",
1055                                         db_path, directory, basename);
1056         else
1057             filename = talloc_asprintf (message, "%s/%s",
1058                                         db_path, basename);
1059
1060         _notmuch_string_list_append (message->filename_list, filename);
1061
1062         talloc_free (local);
1063     }
1064
1065     talloc_free (message->filename_term_list);
1066     message->filename_term_list = NULL;
1067 }
1068
1069 const char *
1070 notmuch_message_get_filename (notmuch_message_t *message)
1071 {
1072     _notmuch_message_ensure_filename_list (message);
1073
1074     if (message->filename_list == NULL)
1075         return NULL;
1076
1077     if (message->filename_list->head == NULL ||
1078         message->filename_list->head->string == NULL)
1079     {
1080         INTERNAL_ERROR ("message with no filename");
1081     }
1082
1083     return message->filename_list->head->string;
1084 }
1085
1086 notmuch_filenames_t *
1087 notmuch_message_get_filenames (notmuch_message_t *message)
1088 {
1089     _notmuch_message_ensure_filename_list (message);
1090
1091     return _notmuch_filenames_create (message, message->filename_list);
1092 }
1093
1094 int
1095 notmuch_message_count_files (notmuch_message_t *message)
1096 {
1097     _notmuch_message_ensure_filename_list (message);
1098
1099     return _notmuch_string_list_length (message->filename_list);
1100 }
1101
1102 notmuch_bool_t
1103 notmuch_message_get_flag (notmuch_message_t *message,
1104                           notmuch_message_flag_t flag)
1105 {
1106     if (flag == NOTMUCH_MESSAGE_FLAG_GHOST &&
1107         ! NOTMUCH_TEST_BIT (message->lazy_flags, flag))
1108         _notmuch_message_ensure_metadata (message, NULL);
1109
1110     return NOTMUCH_TEST_BIT (message->flags, flag);
1111 }
1112
1113 void
1114 notmuch_message_set_flag (notmuch_message_t *message,
1115                           notmuch_message_flag_t flag, notmuch_bool_t enable)
1116 {
1117     if (enable)
1118         NOTMUCH_SET_BIT (&message->flags, flag);
1119     else
1120         NOTMUCH_CLEAR_BIT (&message->flags, flag);
1121     NOTMUCH_SET_BIT (&message->lazy_flags, flag);
1122 }
1123
1124 time_t
1125 notmuch_message_get_date (notmuch_message_t *message)
1126 {
1127     std::string value;
1128
1129     try {
1130         value = message->doc.get_value (NOTMUCH_VALUE_TIMESTAMP);
1131     } catch (Xapian::Error &error) {
1132         _notmuch_database_log(notmuch_message_get_database (message), "A Xapian exception occurred when reading date: %s\n",
1133                  error.get_msg().c_str());
1134         message->notmuch->exception_reported = true;
1135         return 0;
1136     }
1137
1138     if (value.empty ())
1139         /* sortable_unserialise is undefined on empty string */
1140         return 0;
1141     return Xapian::sortable_unserialise (value);
1142 }
1143
1144 notmuch_tags_t *
1145 notmuch_message_get_tags (notmuch_message_t *message)
1146 {
1147     notmuch_tags_t *tags;
1148
1149     _notmuch_message_ensure_metadata (message, message->tag_list);
1150
1151     tags = _notmuch_tags_create (message, message->tag_list);
1152     /* _notmuch_tags_create steals the reference to the tag_list, but
1153      * in this case it's still used by the message, so we add an
1154      * *additional* talloc reference to the list.  As a result, it's
1155      * possible to modify the message tags (which talloc_unlink's the
1156      * current list from the message) while still iterating because
1157      * the iterator will keep the current list alive. */
1158     if (!talloc_reference (message, message->tag_list))
1159         return NULL;
1160
1161     return tags;
1162 }
1163
1164 const char *
1165 _notmuch_message_get_author (notmuch_message_t *message)
1166 {
1167     return message->author;
1168 }
1169
1170 void
1171 _notmuch_message_set_author (notmuch_message_t *message,
1172                             const char *author)
1173 {
1174     if (message->author)
1175         talloc_free(message->author);
1176     message->author = talloc_strdup(message, author);
1177     return;
1178 }
1179
1180 void
1181 _notmuch_message_set_header_values (notmuch_message_t *message,
1182                                     const char *date,
1183                                     const char *from,
1184                                     const char *subject)
1185 {
1186     time_t time_value;
1187
1188     /* GMime really doesn't want to see a NULL date, so protect its
1189      * sensibilities. */
1190     if (date == NULL || *date == '\0') {
1191         time_value = 0;
1192     } else {
1193         time_value = g_mime_utils_header_decode_date_unix (date);
1194         /*
1195          * Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=779923
1196          */
1197         if (time_value < 0)
1198             time_value = 0;
1199     }
1200
1201     message->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
1202                             Xapian::sortable_serialise (time_value));
1203     message->doc.add_value (NOTMUCH_VALUE_FROM, from);
1204     message->doc.add_value (NOTMUCH_VALUE_SUBJECT, subject);
1205     message->modified = true;
1206 }
1207
1208 /* Upgrade a message to support NOTMUCH_FEATURE_LAST_MOD.  The caller
1209  * must call _notmuch_message_sync. */
1210 void
1211 _notmuch_message_upgrade_last_mod (notmuch_message_t *message)
1212 {
1213     /* _notmuch_message_sync will update the last modification
1214      * revision; we just have to ask it to. */
1215     message->modified = true;
1216 }
1217
1218 /* Synchronize changes made to message->doc out into the database. */
1219 void
1220 _notmuch_message_sync (notmuch_message_t *message)
1221 {
1222     Xapian::WritableDatabase *db;
1223
1224     if (message->notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
1225         return;
1226
1227     if (! message->modified)
1228         return;
1229
1230     /* Update the last modification of this message. */
1231     if (message->notmuch->features & NOTMUCH_FEATURE_LAST_MOD)
1232         /* sortable_serialise gives a reasonably compact encoding,
1233          * which directly translates to reduced IO when scanning the
1234          * value stream.  Since it's built for doubles, we only get 53
1235          * effective bits, but that's still enough for the database to
1236          * last a few centuries at 1 million revisions per second. */
1237         message->doc.add_value (NOTMUCH_VALUE_LAST_MOD,
1238                                 Xapian::sortable_serialise (
1239                                     _notmuch_database_new_revision (
1240                                         message->notmuch)));
1241
1242     db = static_cast <Xapian::WritableDatabase *> (message->notmuch->xapian_db);
1243     db->replace_document (message->doc_id, message->doc);
1244     message->modified = false;
1245 }
1246
1247 /* Delete a message document from the database, leaving a ghost
1248  * message in its place */
1249 notmuch_status_t
1250 _notmuch_message_delete (notmuch_message_t *message)
1251 {
1252     notmuch_status_t status;
1253     Xapian::WritableDatabase *db;
1254     const char *mid, *tid, *query_string;
1255     notmuch_message_t *ghost;
1256     notmuch_private_status_t private_status;
1257     notmuch_database_t *notmuch;
1258     notmuch_query_t *query;
1259     unsigned int count = 0;
1260     bool is_ghost;
1261
1262     mid = notmuch_message_get_message_id (message);
1263     tid = notmuch_message_get_thread_id (message);
1264     notmuch = message->notmuch;
1265
1266     status = _notmuch_database_ensure_writable (message->notmuch);
1267     if (status)
1268         return status;
1269
1270     db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
1271     db->delete_document (message->doc_id);
1272
1273     /* if this was a ghost to begin with, we are done */
1274     private_status = _notmuch_message_has_term (message, "type", "ghost", &is_ghost);
1275     if (private_status)
1276         return COERCE_STATUS (private_status,
1277                               "Error trying to determine whether message was a ghost");
1278     if (is_ghost)
1279         return NOTMUCH_STATUS_SUCCESS;
1280
1281     query_string = talloc_asprintf (message, "thread:%s", tid);
1282     query = notmuch_query_create (notmuch, query_string);
1283     if (query == NULL)
1284         return NOTMUCH_STATUS_OUT_OF_MEMORY;
1285     status = notmuch_query_count_messages (query, &count);
1286     if (status) {
1287         notmuch_query_destroy (query);
1288         return status;
1289     }
1290
1291     if (count > 0) {
1292         /* reintroduce a ghost in its place because there are still
1293          * other active messages in this thread: */
1294         ghost = _notmuch_message_create_for_message_id (notmuch, mid, &private_status);
1295         if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
1296             private_status = _notmuch_message_initialize_ghost (ghost, tid);
1297             if (! private_status)
1298                 _notmuch_message_sync (ghost);
1299         } else if (private_status == NOTMUCH_PRIVATE_STATUS_SUCCESS) {
1300             /* this is deeply weird, and we should not have gotten
1301                into this state.  is there a better error message to
1302                return here? */
1303             status = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
1304         }
1305
1306         notmuch_message_destroy (ghost);
1307         status = COERCE_STATUS (private_status, "Error converting to ghost message");
1308     } else {
1309         /* the thread is empty; drop all ghost messages from it */
1310         notmuch_messages_t *messages;
1311         status = _notmuch_query_search_documents (query,
1312                                                   "ghost",
1313                                                   &messages);
1314         if (status == NOTMUCH_STATUS_SUCCESS) {
1315             notmuch_status_t last_error = NOTMUCH_STATUS_SUCCESS;
1316             while (notmuch_messages_valid (messages)) {
1317                 message = notmuch_messages_get (messages);
1318                 status = _notmuch_message_delete (message);
1319                 if (status) /* we'll report the last failure we see;
1320                              * if there is more than one failure, we
1321                              * forget about previous ones */
1322                     last_error = status;
1323                 notmuch_message_destroy (message);
1324                 notmuch_messages_move_to_next (messages);
1325             }
1326             status = last_error;
1327         }
1328     }
1329     notmuch_query_destroy (query);
1330     return status;
1331 }
1332
1333 /* Transform a blank message into a ghost message.  The caller must
1334  * _notmuch_message_sync the message. */
1335 notmuch_private_status_t
1336 _notmuch_message_initialize_ghost (notmuch_message_t *message,
1337                                    const char *thread_id)
1338 {
1339     notmuch_private_status_t status;
1340
1341     status = _notmuch_message_add_term (message, "type", "ghost");
1342     if (status)
1343         return status;
1344     status = _notmuch_message_add_term (message, "thread", thread_id);
1345     if (status)
1346         return status;
1347
1348     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
1349 }
1350
1351 /* Ensure that 'message' is not holding any file object open. Future
1352  * calls to various functions will still automatically open the
1353  * message file as needed.
1354  */
1355 void
1356 _notmuch_message_close (notmuch_message_t *message)
1357 {
1358     if (message->message_file) {
1359         _notmuch_message_file_close (message->message_file);
1360         message->message_file = NULL;
1361     }
1362 }
1363
1364 /* Add a name:value term to 'message', (the actual term will be
1365  * encoded by prefixing the value with a short prefix). See
1366  * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
1367  * names to prefix values.
1368  *
1369  * This change will not be reflected in the database until the next
1370  * call to _notmuch_message_sync. */
1371 notmuch_private_status_t
1372 _notmuch_message_add_term (notmuch_message_t *message,
1373                            const char *prefix_name,
1374                            const char *value)
1375 {
1376
1377     char *term;
1378
1379     if (value == NULL)
1380         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
1381
1382     term = talloc_asprintf (message, "%s%s",
1383                             _find_prefix (prefix_name), value);
1384
1385     if (strlen (term) > NOTMUCH_TERM_MAX)
1386         return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
1387
1388     message->doc.add_term (term, 0);
1389     message->modified = true;
1390
1391     talloc_free (term);
1392
1393     _notmuch_message_invalidate_metadata (message, prefix_name);
1394
1395     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
1396 }
1397
1398 /* Parse 'text' and add a term to 'message' for each parsed word. Each
1399  * term will be added both prefixed (if prefix_name is not NULL) and
1400  * also non-prefixed). */
1401 notmuch_private_status_t
1402 _notmuch_message_gen_terms (notmuch_message_t *message,
1403                             const char *prefix_name,
1404                             const char *text)
1405 {
1406     Xapian::TermGenerator *term_gen = message->notmuch->term_gen;
1407
1408     if (text == NULL)
1409         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
1410
1411     term_gen->set_document (message->doc);
1412
1413     if (prefix_name) {
1414         const char *prefix = _find_prefix (prefix_name);
1415
1416         term_gen->set_termpos (message->termpos);
1417         term_gen->index_text (text, 1, prefix);
1418         /* Create a gap between this an the next terms so they don't
1419          * appear to be a phrase. */
1420         message->termpos = term_gen->get_termpos () + 100;
1421
1422         _notmuch_message_invalidate_metadata (message, prefix_name);
1423     }
1424
1425     term_gen->set_termpos (message->termpos);
1426     term_gen->index_text (text);
1427     /* Create a term gap, as above. */
1428     message->termpos = term_gen->get_termpos () + 100;
1429
1430     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
1431 }
1432
1433 /* Remove a name:value term from 'message', (the actual term will be
1434  * encoded by prefixing the value with a short prefix). See
1435  * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
1436  * names to prefix values.
1437  *
1438  * This change will not be reflected in the database until the next
1439  * call to _notmuch_message_sync. */
1440 notmuch_private_status_t
1441 _notmuch_message_remove_term (notmuch_message_t *message,
1442                               const char *prefix_name,
1443                               const char *value)
1444 {
1445     char *term;
1446
1447     if (value == NULL)
1448         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
1449
1450     term = talloc_asprintf (message, "%s%s",
1451                             _find_prefix (prefix_name), value);
1452
1453     if (strlen (term) > NOTMUCH_TERM_MAX)
1454         return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
1455
1456     try {
1457         message->doc.remove_term (term);
1458         message->modified = true;
1459     } catch (const Xapian::InvalidArgumentError) {
1460         /* We'll let the philosophers try to wrestle with the
1461          * question of whether failing to remove that which was not
1462          * there in the first place is failure. For us, we'll silently
1463          * consider it all good. */
1464     }
1465
1466     talloc_free (term);
1467
1468     _notmuch_message_invalidate_metadata (message, prefix_name);
1469
1470     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
1471 }
1472
1473 notmuch_private_status_t
1474 _notmuch_message_has_term (notmuch_message_t *message,
1475                            const char *prefix_name,
1476                            const char *value,
1477                            bool *result)
1478 {
1479     char *term;
1480     bool out = false;
1481     notmuch_private_status_t status = NOTMUCH_PRIVATE_STATUS_SUCCESS;
1482
1483     if (value == NULL)
1484         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
1485
1486     term = talloc_asprintf (message, "%s%s",
1487                             _find_prefix (prefix_name), value);
1488
1489     if (strlen (term) > NOTMUCH_TERM_MAX)
1490         return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
1491
1492     try {
1493         /* Look for the exact term */
1494         Xapian::TermIterator i = message->doc.termlist_begin ();
1495         i.skip_to (term);
1496         if (i != message->doc.termlist_end () &&
1497             !strcmp ((*i).c_str (), term))
1498             out = true;
1499     } catch (Xapian::Error &error) {
1500         status = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
1501     }
1502     talloc_free (term);
1503
1504     *result = out;
1505     return status;
1506 }
1507
1508 notmuch_status_t
1509 notmuch_message_add_tag (notmuch_message_t *message, const char *tag)
1510 {
1511     notmuch_private_status_t private_status;
1512     notmuch_status_t status;
1513
1514     status = _notmuch_database_ensure_writable (message->notmuch);
1515     if (status)
1516         return status;
1517
1518     if (tag == NULL)
1519         return NOTMUCH_STATUS_NULL_POINTER;
1520
1521     if (strlen (tag) > NOTMUCH_TAG_MAX)
1522         return NOTMUCH_STATUS_TAG_TOO_LONG;
1523
1524     private_status = _notmuch_message_add_term (message, "tag", tag);
1525     if (private_status) {
1526         INTERNAL_ERROR ("_notmuch_message_add_term return unexpected value: %d\n",
1527                         private_status);
1528     }
1529
1530     if (! message->frozen)
1531         _notmuch_message_sync (message);
1532
1533     return NOTMUCH_STATUS_SUCCESS;
1534 }
1535
1536 notmuch_status_t
1537 notmuch_message_remove_tag (notmuch_message_t *message, const char *tag)
1538 {
1539     notmuch_private_status_t private_status;
1540     notmuch_status_t status;
1541
1542     status = _notmuch_database_ensure_writable (message->notmuch);
1543     if (status)
1544         return status;
1545
1546     if (tag == NULL)
1547         return NOTMUCH_STATUS_NULL_POINTER;
1548
1549     if (strlen (tag) > NOTMUCH_TAG_MAX)
1550         return NOTMUCH_STATUS_TAG_TOO_LONG;
1551
1552     private_status = _notmuch_message_remove_term (message, "tag", tag);
1553     if (private_status) {
1554         INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
1555                         private_status);
1556     }
1557
1558     if (! message->frozen)
1559         _notmuch_message_sync (message);
1560
1561     return NOTMUCH_STATUS_SUCCESS;
1562 }
1563
1564 /* Is the given filename within a maildir directory?
1565  *
1566  * Specifically, is the final directory component of 'filename' either
1567  * "cur" or "new". If so, return a pointer to that final directory
1568  * component within 'filename'. If not, return NULL.
1569  *
1570  * A non-NULL return value is guaranteed to be a valid string pointer
1571  * pointing to the characters "new/" or "cur/", (but not
1572  * NUL-terminated).
1573  */
1574 static const char *
1575 _filename_is_in_maildir (const char *filename)
1576 {
1577     const char *slash, *dir = NULL;
1578
1579     /* Find the last '/' separating directory from filename. */
1580     slash = strrchr (filename, '/');
1581     if (slash == NULL)
1582         return NULL;
1583
1584     /* Jump back 4 characters to where the previous '/' will be if the
1585      * directory is named "cur" or "new". */
1586     if (slash - filename < 4)
1587         return NULL;
1588
1589     slash -= 4;
1590
1591     if (*slash != '/')
1592         return NULL;
1593
1594     dir = slash + 1;
1595
1596     if (STRNCMP_LITERAL (dir, "cur/") == 0 ||
1597         STRNCMP_LITERAL (dir, "new/") == 0)
1598     {
1599         return dir;
1600     }
1601
1602     return NULL;
1603 }
1604
1605 static void
1606 _ensure_maildir_flags (notmuch_message_t *message, bool force)
1607 {
1608     const char *flags;
1609     notmuch_filenames_t *filenames;
1610     const char *filename, *dir;
1611     char *combined_flags = talloc_strdup (message, "");
1612     int seen_maildir_info = 0;
1613
1614     if (message->maildir_flags) {
1615         if (force) {
1616             talloc_free (message->maildir_flags);
1617             message->maildir_flags = NULL;
1618         }
1619     }
1620
1621     for (filenames = notmuch_message_get_filenames (message);
1622          notmuch_filenames_valid (filenames);
1623          notmuch_filenames_move_to_next (filenames))
1624     {
1625         filename = notmuch_filenames_get (filenames);
1626         dir = _filename_is_in_maildir (filename);
1627
1628         if (! dir)
1629             continue;
1630
1631         flags = strstr (filename, ":2,");
1632         if (flags) {
1633             seen_maildir_info = 1;
1634             flags += 3;
1635             combined_flags = talloc_strdup_append (combined_flags, flags);
1636         } else if (STRNCMP_LITERAL (dir, "new/") == 0) {
1637             /* Messages are delivered to new/ with no "info" part, but
1638              * they effectively have default maildir flags.  According
1639              * to the spec, we should ignore the info part for
1640              * messages in new/, but some MUAs (mutt) can set maildir
1641              * flags on messages in new/, so we're liberal in what we
1642              * accept. */
1643             seen_maildir_info = 1;
1644         }
1645     }
1646     if (seen_maildir_info)
1647         message->maildir_flags = combined_flags;
1648 }
1649
1650 notmuch_bool_t
1651 notmuch_message_has_maildir_flag (notmuch_message_t *message, char flag)
1652 {
1653     _ensure_maildir_flags (message, false);
1654     return message->maildir_flags && (strchr (message->maildir_flags, flag) != NULL);
1655 }
1656
1657 notmuch_status_t
1658 notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
1659 {
1660     notmuch_status_t status;
1661     unsigned i;
1662
1663     _ensure_maildir_flags (message, true);
1664     /* If none of the filenames have any maildir info field (not even
1665      * an empty info with no flags set) then there's no information to
1666      * go on, so do nothing. */
1667     if (! message->maildir_flags)
1668         return NOTMUCH_STATUS_SUCCESS;
1669
1670     status = notmuch_message_freeze (message);
1671     if (status)
1672         return status;
1673
1674     for (i = 0; i < ARRAY_SIZE(flag2tag); i++) {
1675         if ((strchr (message->maildir_flags, flag2tag[i].flag) != NULL)
1676             ^
1677             flag2tag[i].inverse)
1678         {
1679             status = notmuch_message_add_tag (message, flag2tag[i].tag);
1680         } else {
1681             status = notmuch_message_remove_tag (message, flag2tag[i].tag);
1682         }
1683         if (status)
1684             return status;
1685     }
1686     status = notmuch_message_thaw (message);
1687
1688     return status;
1689 }
1690
1691 /* From the set of tags on 'message' and the flag2tag table, compute a
1692  * set of maildir-flag actions to be taken, (flags that should be
1693  * either set or cleared).
1694  *
1695  * The result is returned as two talloced strings: to_set, and to_clear
1696  */
1697 static void
1698 _get_maildir_flag_actions (notmuch_message_t *message,
1699                            char **to_set_ret,
1700                            char **to_clear_ret)
1701 {
1702     char *to_set, *to_clear;
1703     notmuch_tags_t *tags;
1704     const char *tag;
1705     unsigned i;
1706
1707     to_set = talloc_strdup (message, "");
1708     to_clear = talloc_strdup (message, "");
1709
1710     /* First, find flags for all set tags. */
1711     for (tags = notmuch_message_get_tags (message);
1712          notmuch_tags_valid (tags);
1713          notmuch_tags_move_to_next (tags))
1714     {
1715         tag = notmuch_tags_get (tags);
1716
1717         for (i = 0; i < ARRAY_SIZE (flag2tag); i++) {
1718             if (strcmp (tag, flag2tag[i].tag) == 0) {
1719                 if (flag2tag[i].inverse)
1720                     to_clear = talloc_asprintf_append (to_clear,
1721                                                        "%c",
1722                                                        flag2tag[i].flag);
1723                 else
1724                     to_set = talloc_asprintf_append (to_set,
1725                                                      "%c",
1726                                                      flag2tag[i].flag);
1727             }
1728         }
1729     }
1730
1731     /* Then, find the flags for all tags not present. */
1732     for (i = 0; i < ARRAY_SIZE (flag2tag); i++) {
1733         if (flag2tag[i].inverse) {
1734             if (strchr (to_clear, flag2tag[i].flag) == NULL)
1735                 to_set = talloc_asprintf_append (to_set, "%c", flag2tag[i].flag);
1736         } else {
1737             if (strchr (to_set, flag2tag[i].flag) == NULL)
1738                 to_clear = talloc_asprintf_append (to_clear, "%c", flag2tag[i].flag);
1739         }
1740     }
1741
1742     *to_set_ret = to_set;
1743     *to_clear_ret = to_clear;
1744 }
1745
1746 /* Given 'filename' and a set of maildir flags to set and to clear,
1747  * compute the new maildir filename.
1748  *
1749  * If the existing filename is in the directory "new", the new
1750  * filename will be in the directory "cur", except for the case when
1751  * no flags are changed and the existing filename does not contain
1752  * maildir info (starting with ",2:").
1753  *
1754  * After a sequence of ":2," in the filename, any subsequent
1755  * single-character flags will be added or removed according to the
1756  * characters in flags_to_set and flags_to_clear. Any existing flags
1757  * not mentioned in either string will remain. The final list of flags
1758  * will be in ASCII order.
1759  *
1760  * If the original flags seem invalid, (repeated characters or
1761  * non-ASCII ordering of flags), this function will return NULL
1762  * (meaning that renaming would not be safe and should not occur).
1763  */
1764 static char*
1765 _new_maildir_filename (void *ctx,
1766                        const char *filename,
1767                        const char *flags_to_set,
1768                        const char *flags_to_clear)
1769 {
1770     const char *info, *flags;
1771     unsigned int flag, last_flag;
1772     char *filename_new, *dir;
1773     char flag_map[128];
1774     int flags_in_map = 0;
1775     bool flags_changed = false;
1776     unsigned int i;
1777     char *s;
1778
1779     memset (flag_map, 0, sizeof (flag_map));
1780
1781     info = strstr (filename, ":2,");
1782
1783     if (info == NULL) {
1784         info = filename + strlen(filename);
1785     } else {
1786         /* Loop through existing flags in filename. */
1787         for (flags = info + 3, last_flag = 0;
1788              *flags;
1789              last_flag = flag, flags++)
1790         {
1791             flag = *flags;
1792
1793             /* Original flags not in ASCII order. Abort. */
1794             if (flag < last_flag)
1795                 return NULL;
1796
1797             /* Non-ASCII flag. Abort. */
1798             if (flag > sizeof(flag_map) - 1)
1799                 return NULL;
1800
1801             /* Repeated flag value. Abort. */
1802             if (flag_map[flag])
1803                 return NULL;
1804
1805             flag_map[flag] = 1;
1806             flags_in_map++;
1807         }
1808     }
1809
1810     /* Then set and clear our flags from tags. */
1811     for (flags = flags_to_set; *flags; flags++) {
1812         flag = *flags;
1813         if (flag_map[flag] == 0) {
1814             flag_map[flag] = 1;
1815             flags_in_map++;
1816             flags_changed = true;
1817         }
1818     }
1819
1820     for (flags = flags_to_clear; *flags; flags++) {
1821         flag = *flags;
1822         if (flag_map[flag]) {
1823             flag_map[flag] = 0;
1824             flags_in_map--;
1825             flags_changed = true;
1826         }
1827     }
1828
1829     /* Messages in new/ without maildir info can be kept in new/ if no
1830      * flags have changed. */
1831     dir = (char *) _filename_is_in_maildir (filename);
1832     if (dir && STRNCMP_LITERAL (dir, "new/") == 0 && !*info && !flags_changed)
1833         return talloc_strdup (ctx, filename);
1834
1835     filename_new = (char *) talloc_size (ctx,
1836                                          info - filename +
1837                                          strlen (":2,") + flags_in_map + 1);
1838     if (unlikely (filename_new == NULL))
1839         return NULL;
1840
1841     strncpy (filename_new, filename, info - filename);
1842     filename_new[info - filename] = '\0';
1843
1844     strcat (filename_new, ":2,");
1845
1846     s = filename_new + strlen (filename_new);
1847     for (i = 0; i < sizeof (flag_map); i++)
1848     {
1849         if (flag_map[i]) {
1850             *s = i;
1851             s++;
1852         }
1853     }
1854     *s = '\0';
1855
1856     /* If message is in new/ move it under cur/. */
1857     dir = (char *) _filename_is_in_maildir (filename_new);
1858     if (dir && STRNCMP_LITERAL (dir, "new/") == 0)
1859         memcpy (dir, "cur/", 4);
1860
1861     return filename_new;
1862 }
1863
1864 notmuch_status_t
1865 notmuch_message_tags_to_maildir_flags (notmuch_message_t *message)
1866 {
1867     notmuch_filenames_t *filenames;
1868     const char *filename;
1869     char *filename_new;
1870     char *to_set, *to_clear;
1871     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
1872
1873     _get_maildir_flag_actions (message, &to_set, &to_clear);
1874
1875     for (filenames = notmuch_message_get_filenames (message);
1876          notmuch_filenames_valid (filenames);
1877          notmuch_filenames_move_to_next (filenames))
1878     {
1879         filename = notmuch_filenames_get (filenames);
1880
1881         if (! _filename_is_in_maildir (filename))
1882             continue;
1883
1884         filename_new = _new_maildir_filename (message, filename,
1885                                               to_set, to_clear);
1886         if (filename_new == NULL)
1887             continue;
1888
1889         if (strcmp (filename, filename_new)) {
1890             int err;
1891             notmuch_status_t new_status;
1892
1893             err = rename (filename, filename_new);
1894             if (err)
1895                 continue;
1896
1897             new_status = _notmuch_message_remove_filename (message,
1898                                                            filename);
1899             /* Hold on to only the first error. */
1900             if (! status && new_status
1901                 && new_status != NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) {
1902                 status = new_status;
1903                 continue;
1904             }
1905
1906             new_status = _notmuch_message_add_filename (message,
1907                                                         filename_new);
1908             /* Hold on to only the first error. */
1909             if (! status && new_status) {
1910                 status = new_status;
1911                 continue;
1912             }
1913
1914             _notmuch_message_sync (message);
1915         }
1916
1917         talloc_free (filename_new);
1918     }
1919
1920     talloc_free (to_set);
1921     talloc_free (to_clear);
1922
1923     return status;
1924 }
1925
1926 notmuch_status_t
1927 notmuch_message_remove_all_tags (notmuch_message_t *message)
1928 {
1929     notmuch_private_status_t private_status;
1930     notmuch_status_t status;
1931     notmuch_tags_t *tags;
1932     const char *tag;
1933
1934     status = _notmuch_database_ensure_writable (message->notmuch);
1935     if (status)
1936         return status;
1937
1938     for (tags = notmuch_message_get_tags (message);
1939          notmuch_tags_valid (tags);
1940          notmuch_tags_move_to_next (tags))
1941     {
1942         tag = notmuch_tags_get (tags);
1943
1944         private_status = _notmuch_message_remove_term (message, "tag", tag);
1945         if (private_status) {
1946             INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
1947                             private_status);
1948         }
1949     }
1950
1951     if (! message->frozen)
1952         _notmuch_message_sync (message);
1953
1954     talloc_free (tags);
1955     return NOTMUCH_STATUS_SUCCESS;
1956 }
1957
1958 notmuch_status_t
1959 notmuch_message_freeze (notmuch_message_t *message)
1960 {
1961     notmuch_status_t status;
1962
1963     status = _notmuch_database_ensure_writable (message->notmuch);
1964     if (status)
1965         return status;
1966
1967     message->frozen++;
1968
1969     return NOTMUCH_STATUS_SUCCESS;
1970 }
1971
1972 notmuch_status_t
1973 notmuch_message_thaw (notmuch_message_t *message)
1974 {
1975     notmuch_status_t status;
1976
1977     status = _notmuch_database_ensure_writable (message->notmuch);
1978     if (status)
1979         return status;
1980
1981     if (message->frozen > 0) {
1982         message->frozen--;
1983         if (message->frozen == 0)
1984             _notmuch_message_sync (message);
1985         return NOTMUCH_STATUS_SUCCESS;
1986     } else {
1987         return NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW;
1988     }
1989 }
1990
1991 void
1992 notmuch_message_destroy (notmuch_message_t *message)
1993 {
1994     talloc_free (message);
1995 }
1996
1997 notmuch_database_t *
1998 notmuch_message_get_database (const notmuch_message_t *message)
1999 {
2000     return message->notmuch;
2001 }
2002
2003 static void
2004 _notmuch_message_ensure_property_map (notmuch_message_t *message)
2005 {
2006     notmuch_string_node_t *node;
2007
2008     if (message->property_map)
2009         return;
2010
2011     _notmuch_message_ensure_metadata (message, message->property_term_list);
2012
2013     message->property_map = _notmuch_string_map_create (message);
2014
2015     for (node = message->property_term_list->head; node; node = node->next) {
2016         const char *key;
2017         char *value;
2018
2019         value = strchr(node->string, '=');
2020         if (!value)
2021             INTERNAL_ERROR ("malformed property term");
2022
2023         *value = '\0';
2024         value++;
2025         key = node->string;
2026
2027         _notmuch_string_map_append (message->property_map, key, value);
2028
2029     }
2030
2031     talloc_free (message->property_term_list);
2032     message->property_term_list = NULL;
2033 }
2034
2035 notmuch_string_map_t *
2036 _notmuch_message_property_map (notmuch_message_t *message)
2037 {
2038     _notmuch_message_ensure_property_map (message);
2039
2040     return message->property_map;
2041 }
2042
2043 bool
2044 _notmuch_message_frozen (notmuch_message_t *message)
2045 {
2046     return message->frozen;
2047 }
2048
2049 notmuch_status_t
2050 notmuch_message_reindex (notmuch_message_t *message,
2051                          notmuch_indexopts_t *indexopts)
2052 {
2053     notmuch_database_t *notmuch = NULL;
2054     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
2055     notmuch_private_status_t private_status;
2056     notmuch_filenames_t *orig_filenames = NULL;
2057     const char *orig_thread_id = NULL;
2058     notmuch_message_file_t *message_file = NULL;
2059
2060     int found = 0;
2061
2062     if (message == NULL)
2063         return NOTMUCH_STATUS_NULL_POINTER;
2064
2065     /* Save in case we need to delete message */
2066     orig_thread_id = notmuch_message_get_thread_id (message);
2067     if (!orig_thread_id) {
2068         /* XXX TODO: make up new error return? */
2069         INTERNAL_ERROR ("message without thread-id");
2070     }
2071
2072     /* strdup it because the metadata may be invalidated */
2073     orig_thread_id = talloc_strdup (message, orig_thread_id);
2074
2075     notmuch = notmuch_message_get_database (message);
2076
2077     ret = _notmuch_database_ensure_writable (notmuch);
2078     if (ret)
2079         return ret;
2080
2081     orig_filenames = notmuch_message_get_filenames (message);
2082
2083     private_status = _notmuch_message_remove_indexed_terms (message);
2084     if (private_status) {
2085         ret = COERCE_STATUS(private_status, "error removing terms");
2086         goto DONE;
2087     }
2088
2089     ret = notmuch_message_remove_all_properties_with_prefix (message, "index.");
2090     if (ret)
2091         goto DONE; /* XXX TODO: distinguish from other error returns above? */
2092     if (indexopts && notmuch_indexopts_get_decrypt_policy (indexopts) == NOTMUCH_DECRYPT_FALSE) {
2093         ret = notmuch_message_remove_all_properties (message, "session-key");
2094         if (ret)
2095             goto DONE;
2096     }
2097
2098     /* re-add the filenames with the associated indexopts */
2099     for (; notmuch_filenames_valid (orig_filenames);
2100          notmuch_filenames_move_to_next (orig_filenames)) {
2101
2102         const char *date;
2103         const char *from, *to, *subject;
2104         char *message_id = NULL;
2105         const char *thread_id = NULL;
2106
2107         const char *filename = notmuch_filenames_get (orig_filenames);
2108
2109         message_file = _notmuch_message_file_open (notmuch, filename);
2110         if (message_file == NULL)
2111             continue;
2112
2113         ret = _notmuch_message_file_get_headers (message_file,
2114                                                  &from, &subject, &to, &date,
2115                                                  &message_id);
2116         if (ret)
2117             goto DONE;
2118
2119         /* XXX TODO: deal with changing message id? */
2120
2121         _notmuch_message_add_filename (message, filename);
2122
2123         ret = _notmuch_database_link_message_to_parents (notmuch, message,
2124                                                          message_file,
2125                                                          &thread_id);
2126         if (ret)
2127             goto DONE;
2128
2129         if (thread_id == NULL)
2130             thread_id = orig_thread_id;
2131
2132         _notmuch_message_add_term (message, "thread", thread_id);
2133         /* Take header values only from first filename */
2134         if (found == 0)
2135             _notmuch_message_set_header_values (message, date, from, subject);
2136
2137         ret = _notmuch_message_index_file (message, indexopts, message_file);
2138
2139         if (ret == NOTMUCH_STATUS_FILE_ERROR)
2140             continue;
2141         if (ret)
2142             goto DONE;
2143
2144         found++;
2145         _notmuch_message_file_close (message_file);
2146         message_file = NULL;
2147     }
2148     if (found == 0) {
2149         /* put back thread id to help cleanup */
2150         _notmuch_message_add_term (message, "thread", orig_thread_id);
2151         ret = _notmuch_message_delete (message);
2152     } else {
2153         _notmuch_message_sync (message);
2154     }
2155
2156  DONE:
2157     if (message_file)
2158         _notmuch_message_file_close (message_file);
2159
2160     /* XXX TODO destroy orig_filenames? */
2161     return ret;
2162 }