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