lib: Eliminate some redundant includes of xapian.h
[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 http://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
24 #include <stdint.h>
25
26 #include <gmime/gmime.h>
27
28 struct _notmuch_message {
29     notmuch_database_t *notmuch;
30     Xapian::docid doc_id;
31     int frozen;
32     char *message_id;
33     char *thread_id;
34     char *in_reply_to;
35     char *filename;
36     char *author;
37     notmuch_message_file_t *message_file;
38     notmuch_message_list_t *replies;
39     unsigned long flags;
40
41     Xapian::Document doc;
42 };
43
44 /* We end up having to call the destructor explicitly because we had
45  * to use "placement new" in order to initialize C++ objects within a
46  * block that we allocated with talloc. So C++ is making talloc
47  * slightly less simple to use, (we wouldn't need
48  * talloc_set_destructor at all otherwise).
49  */
50 static int
51 _notmuch_message_destructor (notmuch_message_t *message)
52 {
53     message->doc.~Document ();
54
55     return 0;
56 }
57
58 static notmuch_message_t *
59 _notmuch_message_create_for_document (const void *talloc_owner,
60                                       notmuch_database_t *notmuch,
61                                       unsigned int doc_id,
62                                       Xapian::Document doc,
63                                       notmuch_private_status_t *status)
64 {
65     notmuch_message_t *message;
66
67     if (status)
68         *status = NOTMUCH_PRIVATE_STATUS_SUCCESS;
69
70     message = talloc (talloc_owner, notmuch_message_t);
71     if (unlikely (message == NULL)) {
72         if (status)
73             *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
74         return NULL;
75     }
76
77     message->notmuch = notmuch;
78     message->doc_id = doc_id;
79
80     message->frozen = 0;
81     message->flags = 0;
82
83     /* Each of these will be lazily created as needed. */
84     message->message_id = NULL;
85     message->thread_id = NULL;
86     message->in_reply_to = NULL;
87     message->filename = NULL;
88     message->message_file = NULL;
89     message->author = NULL;
90
91     message->replies = _notmuch_message_list_create (message);
92     if (unlikely (message->replies == NULL)) {
93         if (status)
94             *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
95         return NULL;
96     }
97
98     /* This is C++'s creepy "placement new", which is really just an
99      * ugly way to call a constructor for a pre-allocated object. So
100      * it's really not an error to not be checking for OUT_OF_MEMORY
101      * here, since this "new" isn't actually allocating memory. This
102      * is language-design comedy of the wrong kind. */
103
104     new (&message->doc) Xapian::Document;
105
106     talloc_set_destructor (message, _notmuch_message_destructor);
107
108     message->doc = doc;
109
110     return message;
111 }
112
113 /* Create a new notmuch_message_t object for an existing document in
114  * the database.
115  *
116  * Here, 'talloc owner' is an optional talloc context to which the new
117  * message will belong. This allows for the caller to not bother
118  * calling notmuch_message_destroy on the message, and no that all
119  * memory will be reclaimed with 'talloc_owner' is free. The caller
120  * still can call notmuch_message_destroy when finished with the
121  * message if desired.
122  *
123  * The 'talloc_owner' argument can also be NULL, in which case the
124  * caller *is* responsible for calling notmuch_message_destroy.
125  *
126  * If no document exists in the database with document ID of 'doc_id'
127  * then this function returns NULL and optionally sets *status to
128  * NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND.
129  *
130  * This function can also fail to due lack of available memory,
131  * returning NULL and optionally setting *status to
132  * NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY.
133  *
134  * The caller can pass NULL for status if uninterested in
135  * distinguishing these two cases.
136  */
137 notmuch_message_t *
138 _notmuch_message_create (const void *talloc_owner,
139                          notmuch_database_t *notmuch,
140                          unsigned int doc_id,
141                          notmuch_private_status_t *status)
142 {
143     Xapian::Document doc;
144
145     try {
146         doc = notmuch->xapian_db->get_document (doc_id);
147     } catch (const Xapian::DocNotFoundError &error) {
148         if (status)
149             *status = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
150         return NULL;
151     }
152
153     return _notmuch_message_create_for_document (talloc_owner, notmuch,
154                                                  doc_id, doc, status);
155 }
156
157 /* Create a new notmuch_message_t object for a specific message ID,
158  * (which may or may not already exist in the database).
159  *
160  * The 'notmuch' database will be the talloc owner of the returned
161  * message.
162  *
163  * This function returns a valid notmuch_message_t whether or not
164  * there is already a document in the database with the given message
165  * ID. These two cases can be distinguished by the value of *status:
166  *
167  *
168  *   NOTMUCH_PRIVATE_STATUS_SUCCESS:
169  *
170  *     There is already a document with message ID 'message_id' in the
171  *     database. The returned message can be used to query/modify the
172  *     document.
173  *   NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND:
174  *
175  *     No document with 'message_id' exists in the database. The
176  *     returned message contains a newly created document (not yet
177  *     added to the database) and a document ID that is known not to
178  *     exist in the database. The caller can modify the message, and a
179  *     call to _notmuch_message_sync will add * the document to the
180  *     database.
181  *
182  * If an error occurs, this function will return NULL and *status
183  * will be set as appropriate. (The status pointer argument must
184  * not be NULL.)
185  */
186 notmuch_message_t *
187 _notmuch_message_create_for_message_id (notmuch_database_t *notmuch,
188                                         const char *message_id,
189                                         notmuch_private_status_t *status_ret)
190 {
191     notmuch_message_t *message;
192     Xapian::Document doc;
193     Xapian::WritableDatabase *db;
194     unsigned int doc_id;
195     char *term;
196
197     *status_ret = NOTMUCH_PRIVATE_STATUS_SUCCESS;
198
199     message = notmuch_database_find_message (notmuch, message_id);
200     if (message)
201         return talloc_steal (notmuch, message);
202
203     term = talloc_asprintf (NULL, "%s%s",
204                             _find_prefix ("id"), message_id);
205     if (term == NULL) {
206         *status_ret = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
207         return NULL;
208     }
209
210     if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
211         INTERNAL_ERROR ("Failure to ensure database is writable.");
212
213     db = static_cast<Xapian::WritableDatabase *> (notmuch->xapian_db);
214     try {
215         doc.add_term (term, 0);
216         talloc_free (term);
217
218         doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id);
219
220         doc_id = _notmuch_database_generate_doc_id (notmuch);
221     } catch (const Xapian::Error &error) {
222         fprintf (stderr, "A Xapian exception occurred creating message: %s\n",
223                  error.get_msg().c_str());
224         notmuch->exception_reported = TRUE;
225         *status_ret = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
226         return NULL;
227     }
228
229     message = _notmuch_message_create_for_document (notmuch, notmuch,
230                                                     doc_id, doc, status_ret);
231
232     /* We want to inform the caller that we had to create a new
233      * document. */
234     if (*status_ret == NOTMUCH_PRIVATE_STATUS_SUCCESS)
235         *status_ret = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
236
237     return message;
238 }
239
240 const char *
241 notmuch_message_get_message_id (notmuch_message_t *message)
242 {
243     Xapian::TermIterator i;
244
245     if (message->message_id)
246         return message->message_id;
247
248     i = message->doc.termlist_begin ();
249     i.skip_to (_find_prefix ("id"));
250
251     if (i == message->doc.termlist_end ())
252         INTERNAL_ERROR ("Message with document ID of %d has no message ID.\n",
253                         message->doc_id);
254
255     message->message_id = talloc_strdup (message, (*i).c_str () + 1);
256
257 #if DEBUG_DATABASE_SANITY
258     i++;
259
260     if (i != message->doc.termlist_end () &&
261         strncmp ((*i).c_str (), _find_prefix ("id"),
262                  strlen (_find_prefix ("id"))) == 0)
263     {
264         INTERNAL_ERROR ("Mail (doc_id: %d) has duplicate message IDs",
265                         message->doc_id);
266     }
267 #endif
268
269     return message->message_id;
270 }
271
272 static void
273 _notmuch_message_ensure_message_file (notmuch_message_t *message)
274 {
275     const char *filename;
276
277     if (message->message_file)
278         return;
279
280     filename = notmuch_message_get_filename (message);
281     if (unlikely (filename == NULL))
282         return;
283
284     message->message_file = _notmuch_message_file_open_ctx (message, filename);
285 }
286
287 const char *
288 notmuch_message_get_header (notmuch_message_t *message, const char *header)
289 {
290     _notmuch_message_ensure_message_file (message);
291     if (message->message_file == NULL)
292         return NULL;
293
294     return notmuch_message_file_get_header (message->message_file, header);
295 }
296
297 /* Return the message ID from the In-Reply-To header of 'message'.
298  *
299  * Returns an empty string ("") if 'message' has no In-Reply-To
300  * header.
301  *
302  * Returns NULL if any error occurs.
303  */
304 const char *
305 _notmuch_message_get_in_reply_to (notmuch_message_t *message)
306 {
307     const char *prefix = _find_prefix ("replyto");
308     int prefix_len = strlen (prefix);
309     Xapian::TermIterator i;
310     std::string in_reply_to;
311
312     if (message->in_reply_to)
313         return message->in_reply_to;
314
315     i = message->doc.termlist_begin ();
316     i.skip_to (prefix);
317
318     if (i != message->doc.termlist_end ())
319         in_reply_to = *i;
320
321     /* It's perfectly valid for a message to have no In-Reply-To
322      * header. For these cases, we return an empty string. */
323     if (i == message->doc.termlist_end () ||
324         strncmp (in_reply_to.c_str (), prefix, prefix_len))
325     {
326         message->in_reply_to = talloc_strdup (message, "");
327         return message->in_reply_to;
328     }
329
330     message->in_reply_to = talloc_strdup (message,
331                                           in_reply_to.c_str () + prefix_len);
332
333 #if DEBUG_DATABASE_SANITY
334     i++;
335
336     in_reply_to = *i;
337
338     if (i != message->doc.termlist_end () &&
339         strncmp ((*i).c_str (), prefix, prefix_len) == 0)
340     {
341        INTERNAL_ERROR ("Message %s has duplicate In-Reply-To IDs: %s and %s\n",
342                         notmuch_message_get_message_id (message),
343                         message->in_reply_to,
344                         (*i).c_str () + prefix_len);
345     }
346 #endif
347
348     return message->in_reply_to;
349 }
350
351 const char *
352 notmuch_message_get_thread_id (notmuch_message_t *message)
353 {
354     const char *prefix = _find_prefix ("thread");
355     Xapian::TermIterator i;
356     std::string id;
357
358     /* This code is written with the assumption that "thread" has a
359      * single-character prefix. */
360     assert (strlen (prefix) == 1);
361
362     if (message->thread_id)
363         return message->thread_id;
364
365     i = message->doc.termlist_begin ();
366     i.skip_to (prefix);
367
368     if (i != message->doc.termlist_end ())
369         id = *i;
370
371     if (i == message->doc.termlist_end () || id[0] != *prefix)
372         INTERNAL_ERROR ("Message with document ID of %d has no thread ID.\n",
373                         message->doc_id);
374
375     message->thread_id = talloc_strdup (message, id.c_str () + 1);
376
377 #if DEBUG_DATABASE_SANITY
378     i++;
379     id = *i;
380
381     if (i != message->doc.termlist_end () && id[0] == *prefix)
382     {
383         INTERNAL_ERROR ("Message %s has duplicate thread IDs: %s and %s\n",
384                         notmuch_message_get_message_id (message),
385                         message->thread_id,
386                         id.c_str () + 1);
387     }
388 #endif
389
390     return message->thread_id;
391 }
392
393 void
394 _notmuch_message_add_reply (notmuch_message_t *message,
395                             notmuch_message_node_t *reply)
396 {
397     _notmuch_message_list_append (message->replies, reply);
398 }
399
400 notmuch_messages_t *
401 notmuch_message_get_replies (notmuch_message_t *message)
402 {
403     return _notmuch_messages_create (message->replies);
404 }
405
406 /* Add an additional 'filename' for 'message'.
407  *
408  * This change will not be reflected in the database until the next
409  * call to _notmuch_message_sync. */
410 notmuch_status_t
411 _notmuch_message_add_filename (notmuch_message_t *message,
412                                const char *filename)
413 {
414     notmuch_status_t status;
415     void *local = talloc_new (message);
416     char *direntry;
417
418     if (message->filename) {
419         talloc_free (message->filename);
420         message->filename = NULL;
421     }
422
423     if (filename == NULL)
424         INTERNAL_ERROR ("Message filename cannot be NULL.");
425
426     status = _notmuch_database_filename_to_direntry (local,
427                                                      message->notmuch,
428                                                      filename, &direntry);
429     if (status)
430         return status;
431
432     _notmuch_message_add_term (message, "file-direntry", direntry);
433
434     talloc_free (local);
435
436     return NOTMUCH_STATUS_SUCCESS;
437 }
438
439 char *
440 _notmuch_message_talloc_copy_data (notmuch_message_t *message)
441 {
442     return talloc_strdup (message, message->doc.get_data ().c_str ());
443 }
444
445 void
446 _notmuch_message_clear_data (notmuch_message_t *message)
447 {
448     message->doc.set_data ("");
449 }
450
451 const char *
452 notmuch_message_get_filename (notmuch_message_t *message)
453 {
454     const char *prefix = _find_prefix ("file-direntry");
455     int prefix_len = strlen (prefix);
456     Xapian::TermIterator i;
457     char *colon, *direntry = NULL;
458     const char *db_path, *directory, *basename;
459     unsigned int directory_id;
460     void *local = talloc_new (message);
461
462     if (message->filename)
463         return message->filename;
464
465     i = message->doc.termlist_begin ();
466     i.skip_to (prefix);
467
468     if (i != message->doc.termlist_end ())
469         direntry = talloc_strdup (local, (*i).c_str ());
470
471     if (i == message->doc.termlist_end () ||
472         strncmp (direntry, prefix, prefix_len))
473     {
474         /* A message document created by an old version of notmuch
475          * (prior to rename support) will have the filename in the
476          * data of the document rather than as a file-direntry term.
477          *
478          * It would be nice to do the upgrade of the document directly
479          * here, but the database is likely open in read-only mode. */
480         const char *data;
481
482         data = message->doc.get_data ().c_str ();
483
484         if (data == NULL)
485             INTERNAL_ERROR ("message with no filename");
486
487         message->filename = talloc_strdup (message, data);
488
489         return message->filename;
490     }
491
492     direntry += prefix_len;
493
494     directory_id = strtol (direntry, &colon, 10);
495
496     if (colon == NULL || *colon != ':')
497         INTERNAL_ERROR ("malformed direntry");
498
499     basename = colon + 1;
500
501     *colon = '\0';
502
503     db_path = notmuch_database_get_path (message->notmuch);
504
505     directory = _notmuch_database_get_directory_path (local,
506                                                       message->notmuch,
507                                                       directory_id);
508
509     if (strlen (directory))
510         message->filename = talloc_asprintf (message, "%s/%s/%s",
511                                              db_path, directory, basename);
512     else
513         message->filename = talloc_asprintf (message, "%s/%s",
514                                              db_path, basename);
515     talloc_free ((void *) directory);
516
517     talloc_free (local);
518
519     return message->filename;
520 }
521
522 notmuch_bool_t
523 notmuch_message_get_flag (notmuch_message_t *message,
524                           notmuch_message_flag_t flag)
525 {
526     return message->flags & (1 << flag);
527 }
528
529 void
530 notmuch_message_set_flag (notmuch_message_t *message,
531                           notmuch_message_flag_t flag, notmuch_bool_t enable)
532 {
533     if (enable)
534         message->flags |= (1 << flag);
535     else
536         message->flags &= ~(1 << flag);
537 }
538
539 time_t
540 notmuch_message_get_date (notmuch_message_t *message)
541 {
542     std::string value;
543
544     try {
545         value = message->doc.get_value (NOTMUCH_VALUE_TIMESTAMP);
546     } catch (Xapian::Error &error) {
547         INTERNAL_ERROR ("Failed to read timestamp value from document.");
548         return 0;
549     }
550
551     return Xapian::sortable_unserialise (value);
552 }
553
554 notmuch_tags_t *
555 notmuch_message_get_tags (notmuch_message_t *message)
556 {
557     Xapian::TermIterator i, end;
558     i = message->doc.termlist_begin();
559     end = message->doc.termlist_end();
560     return _notmuch_convert_tags(message, i, end);
561 }
562
563 const char *
564 notmuch_message_get_author (notmuch_message_t *message)
565 {
566     return message->author;
567 }
568
569 void
570 notmuch_message_set_author (notmuch_message_t *message,
571                             const char *author)
572 {
573     if (message->author)
574         talloc_free(message->author);
575     message->author = talloc_strdup(message, author);
576     return;
577 }
578
579 void
580 _notmuch_message_set_date (notmuch_message_t *message,
581                            const char *date)
582 {
583     time_t time_value;
584
585     /* GMime really doesn't want to see a NULL date, so protect its
586      * sensibilities. */
587     if (date == NULL || *date == '\0')
588         time_value = 0;
589     else
590         time_value = g_mime_utils_header_decode_date (date, NULL);
591
592     message->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
593                             Xapian::sortable_serialise (time_value));
594 }
595
596 /* Synchronize changes made to message->doc out into the database. */
597 void
598 _notmuch_message_sync (notmuch_message_t *message)
599 {
600     Xapian::WritableDatabase *db;
601
602     if (message->notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
603         return;
604
605     db = static_cast <Xapian::WritableDatabase *> (message->notmuch->xapian_db);
606     db->replace_document (message->doc_id, message->doc);
607 }
608
609 /* Ensure that 'message' is not holding any file object open. Future
610  * calls to various functions will still automatically open the
611  * message file as needed.
612  */
613 void
614 _notmuch_message_close (notmuch_message_t *message)
615 {
616     if (message->message_file) {
617         notmuch_message_file_close (message->message_file);
618         message->message_file = NULL;
619     }
620 }
621
622 /* Add a name:value term to 'message', (the actual term will be
623  * encoded by prefixing the value with a short prefix). See
624  * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
625  * names to prefix values.
626  *
627  * This change will not be reflected in the database until the next
628  * call to _notmuch_message_sync. */
629 notmuch_private_status_t
630 _notmuch_message_add_term (notmuch_message_t *message,
631                            const char *prefix_name,
632                            const char *value)
633 {
634
635     char *term;
636
637     if (value == NULL)
638         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
639
640     term = talloc_asprintf (message, "%s%s",
641                             _find_prefix (prefix_name), value);
642
643     if (strlen (term) > NOTMUCH_TERM_MAX)
644         return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
645
646     message->doc.add_term (term, 0);
647
648     talloc_free (term);
649
650     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
651 }
652
653 /* Parse 'text' and add a term to 'message' for each parsed word. Each
654  * term will be added both prefixed (if prefix_name is not NULL) and
655  * also unprefixed). */
656 notmuch_private_status_t
657 _notmuch_message_gen_terms (notmuch_message_t *message,
658                             const char *prefix_name,
659                             const char *text)
660 {
661     Xapian::TermGenerator *term_gen = message->notmuch->term_gen;
662
663     if (text == NULL)
664         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
665
666     term_gen->set_document (message->doc);
667
668     if (prefix_name) {
669         const char *prefix = _find_prefix (prefix_name);
670
671         term_gen->index_text (text, 1, prefix);
672     }
673
674     term_gen->index_text (text);
675
676     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
677 }
678
679 /* Remove a name:value term from 'message', (the actual term will be
680  * encoded by prefixing the value with a short prefix). See
681  * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
682  * names to prefix values.
683  *
684  * This change will not be reflected in the database until the next
685  * call to _notmuch_message_sync. */
686 notmuch_private_status_t
687 _notmuch_message_remove_term (notmuch_message_t *message,
688                               const char *prefix_name,
689                               const char *value)
690 {
691     char *term;
692
693     if (value == NULL)
694         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
695
696     term = talloc_asprintf (message, "%s%s",
697                             _find_prefix (prefix_name), value);
698
699     if (strlen (term) > NOTMUCH_TERM_MAX)
700         return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
701
702     try {
703         message->doc.remove_term (term);
704     } catch (const Xapian::InvalidArgumentError) {
705         /* We'll let the philosopher's try to wrestle with the
706          * question of whether failing to remove that which was not
707          * there in the first place is failure. For us, we'll silently
708          * consider it all good. */
709     }
710
711     talloc_free (term);
712
713     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
714 }
715
716 notmuch_status_t
717 notmuch_message_add_tag (notmuch_message_t *message, const char *tag)
718 {
719     notmuch_private_status_t private_status;
720     notmuch_status_t status;
721
722     status = _notmuch_database_ensure_writable (message->notmuch);
723     if (status)
724         return status;
725
726     if (tag == NULL)
727         return NOTMUCH_STATUS_NULL_POINTER;
728
729     if (strlen (tag) > NOTMUCH_TAG_MAX)
730         return NOTMUCH_STATUS_TAG_TOO_LONG;
731
732     private_status = _notmuch_message_add_term (message, "tag", tag);
733     if (private_status) {
734         INTERNAL_ERROR ("_notmuch_message_add_term return unexpected value: %d\n",
735                         private_status);
736     }
737
738     if (! message->frozen)
739         _notmuch_message_sync (message);
740
741     return NOTMUCH_STATUS_SUCCESS;
742 }
743
744 notmuch_status_t
745 notmuch_message_remove_tag (notmuch_message_t *message, const char *tag)
746 {
747     notmuch_private_status_t private_status;
748     notmuch_status_t status;
749
750     status = _notmuch_database_ensure_writable (message->notmuch);
751     if (status)
752         return status;
753
754     if (tag == NULL)
755         return NOTMUCH_STATUS_NULL_POINTER;
756
757     if (strlen (tag) > NOTMUCH_TAG_MAX)
758         return NOTMUCH_STATUS_TAG_TOO_LONG;
759
760     private_status = _notmuch_message_remove_term (message, "tag", tag);
761     if (private_status) {
762         INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
763                         private_status);
764     }
765
766     if (! message->frozen)
767         _notmuch_message_sync (message);
768
769     return NOTMUCH_STATUS_SUCCESS;
770 }
771
772 notmuch_status_t
773 notmuch_message_remove_all_tags (notmuch_message_t *message)
774 {
775     notmuch_private_status_t private_status;
776     notmuch_status_t status;
777     notmuch_tags_t *tags;
778     const char *tag;
779
780     status = _notmuch_database_ensure_writable (message->notmuch);
781     if (status)
782         return status;
783
784     for (tags = notmuch_message_get_tags (message);
785          notmuch_tags_valid (tags);
786          notmuch_tags_move_to_next (tags))
787     {
788         tag = notmuch_tags_get (tags);
789
790         private_status = _notmuch_message_remove_term (message, "tag", tag);
791         if (private_status) {
792             INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
793                             private_status);
794         }
795     }
796
797     if (! message->frozen)
798         _notmuch_message_sync (message);
799
800     return NOTMUCH_STATUS_SUCCESS;
801 }
802
803 notmuch_status_t
804 notmuch_message_freeze (notmuch_message_t *message)
805 {
806     notmuch_status_t status;
807
808     status = _notmuch_database_ensure_writable (message->notmuch);
809     if (status)
810         return status;
811
812     message->frozen++;
813
814     return NOTMUCH_STATUS_SUCCESS;
815 }
816
817 notmuch_status_t
818 notmuch_message_thaw (notmuch_message_t *message)
819 {
820     notmuch_status_t status;
821
822     status = _notmuch_database_ensure_writable (message->notmuch);
823     if (status)
824         return status;
825
826     if (message->frozen > 0) {
827         message->frozen--;
828         if (message->frozen == 0)
829             _notmuch_message_sync (message);
830         return NOTMUCH_STATUS_SUCCESS;
831     } else {
832         return NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW;
833     }
834 }
835
836 void
837 notmuch_message_destroy (notmuch_message_t *message)
838 {
839     talloc_free (message);
840 }