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