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