Make maildir synchronization configurable
[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 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
45
46 struct maildir_flag_tag {
47     char flag;
48     const char *tag;
49     bool inverse;
50 };
51
52 /* ASCII ordered table of Maildir flags and associated tags */
53 struct maildir_flag_tag flag2tag[] = {
54     { 'D', "draft",   false},
55     { 'F', "flagged", false},
56     { 'P', "passed",  false},
57     { 'R', "replied", false},
58     { 'S', "unread",  true },
59     { 'T', "deleted", false},
60 };
61
62 /* We end up having to call the destructor explicitly because we had
63  * to use "placement new" in order to initialize C++ objects within a
64  * block that we allocated with talloc. So C++ is making talloc
65  * slightly less simple to use, (we wouldn't need
66  * talloc_set_destructor at all otherwise).
67  */
68 static int
69 _notmuch_message_destructor (notmuch_message_t *message)
70 {
71     message->doc.~Document ();
72
73     return 0;
74 }
75
76 static notmuch_message_t *
77 _notmuch_message_create_for_document (const void *talloc_owner,
78                                       notmuch_database_t *notmuch,
79                                       unsigned int doc_id,
80                                       Xapian::Document doc,
81                                       notmuch_private_status_t *status)
82 {
83     notmuch_message_t *message;
84
85     if (status)
86         *status = NOTMUCH_PRIVATE_STATUS_SUCCESS;
87
88     message = talloc (talloc_owner, notmuch_message_t);
89     if (unlikely (message == NULL)) {
90         if (status)
91             *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
92         return NULL;
93     }
94
95     message->notmuch = notmuch;
96     message->doc_id = doc_id;
97
98     message->frozen = 0;
99     message->flags = 0;
100
101     /* Each of these will be lazily created as needed. */
102     message->message_id = NULL;
103     message->thread_id = NULL;
104     message->in_reply_to = NULL;
105     message->filename = NULL;
106     message->message_file = NULL;
107     message->author = NULL;
108
109     message->replies = _notmuch_message_list_create (message);
110     if (unlikely (message->replies == NULL)) {
111         if (status)
112             *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
113         return NULL;
114     }
115
116     /* This is C++'s creepy "placement new", which is really just an
117      * ugly way to call a constructor for a pre-allocated object. So
118      * it's really not an error to not be checking for OUT_OF_MEMORY
119      * here, since this "new" isn't actually allocating memory. This
120      * is language-design comedy of the wrong kind. */
121
122     new (&message->doc) Xapian::Document;
123
124     talloc_set_destructor (message, _notmuch_message_destructor);
125
126     message->doc = doc;
127
128     return message;
129 }
130
131 /* Create a new notmuch_message_t object for an existing document in
132  * the database.
133  *
134  * Here, 'talloc owner' is an optional talloc context to which the new
135  * message will belong. This allows for the caller to not bother
136  * calling notmuch_message_destroy on the message, and no that all
137  * memory will be reclaimed with 'talloc_owner' is free. The caller
138  * still can call notmuch_message_destroy when finished with the
139  * message if desired.
140  *
141  * The 'talloc_owner' argument can also be NULL, in which case the
142  * caller *is* responsible for calling notmuch_message_destroy.
143  *
144  * If no document exists in the database with document ID of 'doc_id'
145  * then this function returns NULL and optionally sets *status to
146  * NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND.
147  *
148  * This function can also fail to due lack of available memory,
149  * returning NULL and optionally setting *status to
150  * NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY.
151  *
152  * The caller can pass NULL for status if uninterested in
153  * distinguishing these two cases.
154  */
155 notmuch_message_t *
156 _notmuch_message_create (const void *talloc_owner,
157                          notmuch_database_t *notmuch,
158                          unsigned int doc_id,
159                          notmuch_private_status_t *status)
160 {
161     Xapian::Document doc;
162
163     try {
164         doc = notmuch->xapian_db->get_document (doc_id);
165     } catch (const Xapian::DocNotFoundError &error) {
166         if (status)
167             *status = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
168         return NULL;
169     }
170
171     return _notmuch_message_create_for_document (talloc_owner, notmuch,
172                                                  doc_id, doc, status);
173 }
174
175 /* Create a new notmuch_message_t object for a specific message ID,
176  * (which may or may not already exist in the database).
177  *
178  * The 'notmuch' database will be the talloc owner of the returned
179  * message.
180  *
181  * This function returns a valid notmuch_message_t whether or not
182  * there is already a document in the database with the given message
183  * ID. These two cases can be distinguished by the value of *status:
184  *
185  *
186  *   NOTMUCH_PRIVATE_STATUS_SUCCESS:
187  *
188  *     There is already a document with message ID 'message_id' in the
189  *     database. The returned message can be used to query/modify the
190  *     document.
191  *   NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND:
192  *
193  *     No document with 'message_id' exists in the database. The
194  *     returned message contains a newly created document (not yet
195  *     added to the database) and a document ID that is known not to
196  *     exist in the database. The caller can modify the message, and a
197  *     call to _notmuch_message_sync will add * the document to the
198  *     database.
199  *
200  * If an error occurs, this function will return NULL and *status
201  * will be set as appropriate. (The status pointer argument must
202  * not be NULL.)
203  */
204 notmuch_message_t *
205 _notmuch_message_create_for_message_id (notmuch_database_t *notmuch,
206                                         const char *message_id,
207                                         notmuch_private_status_t *status_ret)
208 {
209     notmuch_message_t *message;
210     Xapian::Document doc;
211     Xapian::WritableDatabase *db;
212     unsigned int doc_id;
213     char *term;
214
215     *status_ret = NOTMUCH_PRIVATE_STATUS_SUCCESS;
216
217     message = notmuch_database_find_message (notmuch, message_id);
218     if (message)
219         return talloc_steal (notmuch, message);
220
221     term = talloc_asprintf (NULL, "%s%s",
222                             _find_prefix ("id"), message_id);
223     if (term == NULL) {
224         *status_ret = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
225         return NULL;
226     }
227
228     if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
229         INTERNAL_ERROR ("Failure to ensure database is writable.");
230
231     db = static_cast<Xapian::WritableDatabase *> (notmuch->xapian_db);
232     try {
233         doc.add_term (term, 0);
234         talloc_free (term);
235
236         doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id);
237
238         doc_id = _notmuch_database_generate_doc_id (notmuch);
239     } catch (const Xapian::Error &error) {
240         fprintf (stderr, "A Xapian exception occurred creating message: %s\n",
241                  error.get_msg().c_str());
242         notmuch->exception_reported = TRUE;
243         *status_ret = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
244         return NULL;
245     }
246
247     message = _notmuch_message_create_for_document (notmuch, notmuch,
248                                                     doc_id, doc, status_ret);
249
250     /* We want to inform the caller that we had to create a new
251      * document. */
252     if (*status_ret == NOTMUCH_PRIVATE_STATUS_SUCCESS)
253         *status_ret = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
254
255     return message;
256 }
257
258 const char *
259 notmuch_message_get_message_id (notmuch_message_t *message)
260 {
261     Xapian::TermIterator i;
262
263     if (message->message_id)
264         return message->message_id;
265
266     i = message->doc.termlist_begin ();
267     i.skip_to (_find_prefix ("id"));
268
269     if (i == message->doc.termlist_end ())
270         INTERNAL_ERROR ("Message with document ID of %d has no message ID.\n",
271                         message->doc_id);
272
273     message->message_id = talloc_strdup (message, (*i).c_str () + 1);
274
275 #if DEBUG_DATABASE_SANITY
276     i++;
277
278     if (i != message->doc.termlist_end () &&
279         strncmp ((*i).c_str (), _find_prefix ("id"),
280                  strlen (_find_prefix ("id"))) == 0)
281     {
282         INTERNAL_ERROR ("Mail (doc_id: %d) has duplicate message IDs",
283                         message->doc_id);
284     }
285 #endif
286
287     return message->message_id;
288 }
289
290 static void
291 _notmuch_message_ensure_message_file (notmuch_message_t *message)
292 {
293     const char *filename;
294
295     if (message->message_file)
296         return;
297
298     filename = notmuch_message_get_filename (message);
299     if (unlikely (filename == NULL))
300         return;
301
302     message->message_file = _notmuch_message_file_open_ctx (message, filename);
303 }
304
305 const char *
306 notmuch_message_get_header (notmuch_message_t *message, const char *header)
307 {
308     _notmuch_message_ensure_message_file (message);
309     if (message->message_file == NULL)
310         return NULL;
311
312     return notmuch_message_file_get_header (message->message_file, header);
313 }
314
315 /* Return the message ID from the In-Reply-To header of 'message'.
316  *
317  * Returns an empty string ("") if 'message' has no In-Reply-To
318  * header.
319  *
320  * Returns NULL if any error occurs.
321  */
322 const char *
323 _notmuch_message_get_in_reply_to (notmuch_message_t *message)
324 {
325     const char *prefix = _find_prefix ("replyto");
326     int prefix_len = strlen (prefix);
327     Xapian::TermIterator i;
328     std::string in_reply_to;
329
330     if (message->in_reply_to)
331         return message->in_reply_to;
332
333     i = message->doc.termlist_begin ();
334     i.skip_to (prefix);
335
336     if (i != message->doc.termlist_end ())
337         in_reply_to = *i;
338
339     /* It's perfectly valid for a message to have no In-Reply-To
340      * header. For these cases, we return an empty string. */
341     if (i == message->doc.termlist_end () ||
342         strncmp (in_reply_to.c_str (), prefix, prefix_len))
343     {
344         message->in_reply_to = talloc_strdup (message, "");
345         return message->in_reply_to;
346     }
347
348     message->in_reply_to = talloc_strdup (message,
349                                           in_reply_to.c_str () + prefix_len);
350
351 #if DEBUG_DATABASE_SANITY
352     i++;
353
354     in_reply_to = *i;
355
356     if (i != message->doc.termlist_end () &&
357         strncmp ((*i).c_str (), prefix, prefix_len) == 0)
358     {
359        INTERNAL_ERROR ("Message %s has duplicate In-Reply-To IDs: %s and %s\n",
360                         notmuch_message_get_message_id (message),
361                         message->in_reply_to,
362                         (*i).c_str () + prefix_len);
363     }
364 #endif
365
366     return message->in_reply_to;
367 }
368
369 const char *
370 notmuch_message_get_thread_id (notmuch_message_t *message)
371 {
372     const char *prefix = _find_prefix ("thread");
373     Xapian::TermIterator i;
374     std::string id;
375
376     /* This code is written with the assumption that "thread" has a
377      * single-character prefix. */
378     assert (strlen (prefix) == 1);
379
380     if (message->thread_id)
381         return message->thread_id;
382
383     i = message->doc.termlist_begin ();
384     i.skip_to (prefix);
385
386     if (i != message->doc.termlist_end ())
387         id = *i;
388
389     if (i == message->doc.termlist_end () || id[0] != *prefix)
390         INTERNAL_ERROR ("Message with document ID of %d has no thread ID.\n",
391                         message->doc_id);
392
393     message->thread_id = talloc_strdup (message, id.c_str () + 1);
394
395 #if DEBUG_DATABASE_SANITY
396     i++;
397     id = *i;
398
399     if (i != message->doc.termlist_end () && id[0] == *prefix)
400     {
401         INTERNAL_ERROR ("Message %s has duplicate thread IDs: %s and %s\n",
402                         notmuch_message_get_message_id (message),
403                         message->thread_id,
404                         id.c_str () + 1);
405     }
406 #endif
407
408     return message->thread_id;
409 }
410
411 void
412 _notmuch_message_add_reply (notmuch_message_t *message,
413                             notmuch_message_node_t *reply)
414 {
415     _notmuch_message_list_append (message->replies, reply);
416 }
417
418 notmuch_messages_t *
419 notmuch_message_get_replies (notmuch_message_t *message)
420 {
421     return _notmuch_messages_create (message->replies);
422 }
423
424 /* Add an additional 'filename' for 'message'.
425  *
426  * This change will not be reflected in the database until the next
427  * call to _notmuch_message_sync. */
428 notmuch_status_t
429 _notmuch_message_add_filename (notmuch_message_t *message,
430                                const char *filename)
431 {
432     notmuch_status_t status;
433     void *local = talloc_new (message);
434     char *direntry;
435
436     if (message->filename) {
437         talloc_free (message->filename);
438         message->filename = NULL;
439     }
440
441     if (filename == NULL)
442         INTERNAL_ERROR ("Message filename cannot be NULL.");
443
444     status = _notmuch_database_filename_to_direntry (local,
445                                                      message->notmuch,
446                                                      filename, &direntry);
447     if (status)
448         return status;
449
450     _notmuch_message_add_term (message, "file-direntry", direntry);
451
452     talloc_free (local);
453
454     return NOTMUCH_STATUS_SUCCESS;
455 }
456
457 char *
458 _notmuch_message_talloc_copy_data (notmuch_message_t *message)
459 {
460     return talloc_strdup (message, message->doc.get_data ().c_str ());
461 }
462
463 void
464 _notmuch_message_clear_data (notmuch_message_t *message)
465 {
466     message->doc.set_data ("");
467 }
468
469 const char *
470 notmuch_message_get_filename (notmuch_message_t *message)
471 {
472     const char *prefix = _find_prefix ("file-direntry");
473     int prefix_len = strlen (prefix);
474     Xapian::TermIterator i;
475     char *colon, *direntry = NULL;
476     const char *db_path, *directory, *basename;
477     unsigned int directory_id;
478     void *local = talloc_new (message);
479
480     if (message->filename)
481         return message->filename;
482
483     i = message->doc.termlist_begin ();
484     i.skip_to (prefix);
485
486     if (i != message->doc.termlist_end ())
487         direntry = talloc_strdup (local, (*i).c_str ());
488
489     if (i == message->doc.termlist_end () ||
490         strncmp (direntry, prefix, prefix_len))
491     {
492         /* A message document created by an old version of notmuch
493          * (prior to rename support) will have the filename in the
494          * data of the document rather than as a file-direntry term.
495          *
496          * It would be nice to do the upgrade of the document directly
497          * here, but the database is likely open in read-only mode. */
498         const char *data;
499
500         data = message->doc.get_data ().c_str ();
501
502         if (data == NULL)
503             INTERNAL_ERROR ("message with no filename");
504
505         message->filename = talloc_strdup (message, data);
506
507         return message->filename;
508     }
509
510     direntry += prefix_len;
511
512     directory_id = strtol (direntry, &colon, 10);
513
514     if (colon == NULL || *colon != ':')
515         INTERNAL_ERROR ("malformed direntry");
516
517     basename = colon + 1;
518
519     *colon = '\0';
520
521     db_path = notmuch_database_get_path (message->notmuch);
522
523     directory = _notmuch_database_get_directory_path (local,
524                                                       message->notmuch,
525                                                       directory_id);
526
527     if (strlen (directory))
528         message->filename = talloc_asprintf (message, "%s/%s/%s",
529                                              db_path, directory, basename);
530     else
531         message->filename = talloc_asprintf (message, "%s/%s",
532                                              db_path, basename);
533     talloc_free ((void *) directory);
534
535     talloc_free (local);
536
537     return message->filename;
538 }
539
540 notmuch_bool_t
541 notmuch_message_get_flag (notmuch_message_t *message,
542                           notmuch_message_flag_t flag)
543 {
544     return message->flags & (1 << flag);
545 }
546
547 void
548 notmuch_message_set_flag (notmuch_message_t *message,
549                           notmuch_message_flag_t flag, notmuch_bool_t enable)
550 {
551     if (enable)
552         message->flags |= (1 << flag);
553     else
554         message->flags &= ~(1 << flag);
555 }
556
557 time_t
558 notmuch_message_get_date (notmuch_message_t *message)
559 {
560     std::string value;
561
562     try {
563         value = message->doc.get_value (NOTMUCH_VALUE_TIMESTAMP);
564     } catch (Xapian::Error &error) {
565         INTERNAL_ERROR ("Failed to read timestamp value from document.");
566         return 0;
567     }
568
569     return Xapian::sortable_unserialise (value);
570 }
571
572 notmuch_tags_t *
573 notmuch_message_get_tags (notmuch_message_t *message)
574 {
575     Xapian::TermIterator i, end;
576     i = message->doc.termlist_begin();
577     end = message->doc.termlist_end();
578     return _notmuch_convert_tags(message, i, end);
579 }
580
581 const char *
582 notmuch_message_get_author (notmuch_message_t *message)
583 {
584     return message->author;
585 }
586
587 void
588 notmuch_message_set_author (notmuch_message_t *message,
589                             const char *author)
590 {
591     if (message->author)
592         talloc_free(message->author);
593     message->author = talloc_strdup(message, author);
594     return;
595 }
596
597 void
598 _notmuch_message_set_date (notmuch_message_t *message,
599                            const char *date)
600 {
601     time_t time_value;
602
603     /* GMime really doesn't want to see a NULL date, so protect its
604      * sensibilities. */
605     if (date == NULL || *date == '\0')
606         time_value = 0;
607     else
608         time_value = g_mime_utils_header_decode_date (date, NULL);
609
610     message->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
611                             Xapian::sortable_serialise (time_value));
612 }
613
614 static notmuch_private_status_t
615 _notmuch_message_tags_to_maildir (notmuch_message_t *message);
616
617 /* Synchronize changes made to message->doc out into the database. */
618 void
619 _notmuch_message_sync (notmuch_message_t *message)
620 {
621     Xapian::WritableDatabase *db;
622     notmuch_private_status_t status;
623
624     if (message->notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
625         return;
626
627     if (message->notmuch->maildir_sync &&
628         !notmuch_message_get_flag(message, NOTMUCH_MESSAGE_FLAG_TAGS_INVALID)) {
629         status = _notmuch_message_tags_to_maildir (message);
630         if (status != NOTMUCH_PRIVATE_STATUS_SUCCESS) {
631             fprintf (stderr, "Error: Cannot sync tags to maildir (%s)\n",
632                      notmuch_status_to_string ((notmuch_status_t)status));
633             /* Exit to avoid unsynchronized mailstore. */
634             exit(1);
635         }
636     }
637     db = static_cast <Xapian::WritableDatabase *> (message->notmuch->xapian_db);
638     db->replace_document (message->doc_id, message->doc);
639 }
640
641 /* Ensure that 'message' is not holding any file object open. Future
642  * calls to various functions will still automatically open the
643  * message file as needed.
644  */
645 void
646 _notmuch_message_close (notmuch_message_t *message)
647 {
648     if (message->message_file) {
649         notmuch_message_file_close (message->message_file);
650         message->message_file = NULL;
651     }
652 }
653
654 /* Add a name:value term to '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_add_term (notmuch_message_t *message,
663                            const char *prefix_name,
664                            const char *value)
665 {
666
667     char *term;
668
669     if (value == NULL)
670         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
671
672     term = talloc_asprintf (message, "%s%s",
673                             _find_prefix (prefix_name), value);
674
675     if (strlen (term) > NOTMUCH_TERM_MAX)
676         return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
677
678     message->doc.add_term (term, 0);
679
680     talloc_free (term);
681
682     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
683 }
684
685 /* Parse 'text' and add a term to 'message' for each parsed word. Each
686  * term will be added both prefixed (if prefix_name is not NULL) and
687  * also unprefixed). */
688 notmuch_private_status_t
689 _notmuch_message_gen_terms (notmuch_message_t *message,
690                             const char *prefix_name,
691                             const char *text)
692 {
693     Xapian::TermGenerator *term_gen = message->notmuch->term_gen;
694
695     if (text == NULL)
696         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
697
698     term_gen->set_document (message->doc);
699
700     if (prefix_name) {
701         const char *prefix = _find_prefix (prefix_name);
702
703         term_gen->index_text (text, 1, prefix);
704     }
705
706     term_gen->index_text (text);
707
708     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
709 }
710
711 /* Remove a name:value term from 'message', (the actual term will be
712  * encoded by prefixing the value with a short prefix). See
713  * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
714  * names to prefix values.
715  *
716  * This change will not be reflected in the database until the next
717  * call to _notmuch_message_sync. */
718 notmuch_private_status_t
719 _notmuch_message_remove_term (notmuch_message_t *message,
720                               const char *prefix_name,
721                               const char *value)
722 {
723     char *term;
724
725     if (value == NULL)
726         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
727
728     term = talloc_asprintf (message, "%s%s",
729                             _find_prefix (prefix_name), value);
730
731     if (strlen (term) > NOTMUCH_TERM_MAX)
732         return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
733
734     try {
735         message->doc.remove_term (term);
736     } catch (const Xapian::InvalidArgumentError) {
737         /* We'll let the philosopher's try to wrestle with the
738          * question of whether failing to remove that which was not
739          * there in the first place is failure. For us, we'll silently
740          * consider it all good. */
741     }
742
743     talloc_free (term);
744
745     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
746 }
747
748 /* Change the message filename stored in the database.
749  *
750  * This change will not be reflected in the database until the next
751  * call to _notmuch_message_sync.
752  */
753 notmuch_private_status_t
754 _notmuch_message_rename (notmuch_message_t *message,
755                          const char *new_filename)
756 {
757     void *local = talloc_new (message);
758     char *direntry;
759     Xapian::PostingIterator i, end;
760     Xapian::Document document;
761     notmuch_private_status_t pstatus;
762     notmuch_status_t status;
763     const char *old_filename;
764
765     old_filename = notmuch_message_get_filename(message);
766     old_filename = talloc_reference(local, old_filename);
767     if (unlikely(!old_filename))
768         return NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
769
770     status = _notmuch_message_add_filename (message, new_filename);
771     if (status)
772         return (notmuch_private_status_t)status;
773
774     status = _notmuch_database_filename_to_direntry (local, message->notmuch,
775                                                      old_filename, &direntry);
776     if (status)
777         return (notmuch_private_status_t)status;
778
779     pstatus = _notmuch_message_remove_term (message, "file-direntry", direntry);
780
781     talloc_free (local);
782
783     return pstatus;
784 }
785
786 notmuch_status_t
787 notmuch_message_add_tag (notmuch_message_t *message, const char *tag)
788 {
789     notmuch_private_status_t private_status;
790     notmuch_status_t status;
791
792     status = _notmuch_database_ensure_writable (message->notmuch);
793     if (status)
794         return status;
795
796     if (tag == NULL)
797         return NOTMUCH_STATUS_NULL_POINTER;
798
799     if (strlen (tag) > NOTMUCH_TAG_MAX)
800         return NOTMUCH_STATUS_TAG_TOO_LONG;
801
802     private_status = _notmuch_message_add_term (message, "tag", tag);
803     if (private_status) {
804         INTERNAL_ERROR ("_notmuch_message_add_term return unexpected value: %d\n",
805                         private_status);
806     }
807
808     if (! message->frozen)
809         _notmuch_message_sync (message);
810
811     return NOTMUCH_STATUS_SUCCESS;
812 }
813
814 notmuch_status_t
815 notmuch_message_remove_tag (notmuch_message_t *message, const char *tag)
816 {
817     notmuch_private_status_t private_status;
818     notmuch_status_t status;
819
820     status = _notmuch_database_ensure_writable (message->notmuch);
821     if (status)
822         return status;
823
824     if (tag == NULL)
825         return NOTMUCH_STATUS_NULL_POINTER;
826
827     if (strlen (tag) > NOTMUCH_TAG_MAX)
828         return NOTMUCH_STATUS_TAG_TOO_LONG;
829
830     private_status = _notmuch_message_remove_term (message, "tag", tag);
831     if (private_status) {
832         INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
833                         private_status);
834     }
835
836     if (! message->frozen)
837         _notmuch_message_sync (message);
838
839     return NOTMUCH_STATUS_SUCCESS;
840 }
841
842 notmuch_status_t
843 notmuch_message_maildir_to_tags (notmuch_message_t *message, const char *filename)
844 {
845     const char *flags, *p;
846     char f;
847     bool valid, unread;
848     unsigned i;
849     notmuch_status_t status;
850
851     flags = strstr (filename, ":2,");
852     if (!flags)
853         return NOTMUCH_STATUS_FILE_NOT_EMAIL;
854     flags += 3;
855
856     /*  Check that the letters are valid Maildir flags */
857     f = 0;
858     valid = true;
859     for (p=flags; valid && *p; p++) {
860         switch (*p) {
861         case 'P':
862         case 'R':
863         case 'S':
864         case 'T':
865         case 'D':
866         case 'F':
867             if (*p > f) f=*p;
868             else valid = false;
869         break;
870         default:
871             valid = false;
872         }
873     }
874     if (!valid) {
875         fprintf (stderr, "Warning: Invalid maildir flags in filename %s\n", filename);
876         return NOTMUCH_STATUS_FILE_NOT_EMAIL;
877     }
878
879     status = notmuch_message_freeze (message);
880     if (status)
881         return status;
882     unread = true;
883     for (i = 0; i < ARRAY_SIZE(flag2tag); i++) {
884         if ((strchr (flags, flag2tag[i].flag) != NULL) ^ flag2tag[i].inverse) {
885             status = notmuch_message_add_tag (message, flag2tag[i].tag);
886         } else {
887             status = notmuch_message_remove_tag (message, flag2tag[i].tag);
888         }
889         if (status)
890             return status;
891     }
892     status = notmuch_message_thaw (message);
893
894     /* From now on, we can synchronize the tags from the database to
895      * the mailstore. */
896     notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_TAGS_INVALID, FALSE);
897     return status;
898 }
899
900 static void
901 maildir_get_new_flags(notmuch_message_t *message, char *flags)
902 {
903     notmuch_tags_t *tags;
904     const char *tag;
905     unsigned i;
906     char *p;
907
908     for (i = 0; i < ARRAY_SIZE(flag2tag); i++)
909         flags[i] = flag2tag[i].inverse ? flag2tag[i].flag : '\0';
910
911     for (tags = notmuch_message_get_tags (message);
912          notmuch_tags_valid (tags);
913          notmuch_tags_move_to_next (tags))
914     {
915         tag = notmuch_tags_get (tags);
916         for (i = 0; i < ARRAY_SIZE(flag2tag); i++) {
917             if (strcmp(tag, flag2tag[i].tag) == 0)
918                 flags[i] = flag2tag[i].inverse ? '\0' : flag2tag[i].flag;
919         }
920     }
921
922     p = flags;
923     for (i = 0; i < ARRAY_SIZE(flag2tag); i++) {
924         if (flags[i])
925             *p++ = flags[i];
926     }
927     *p = '\0';
928 }
929
930 static char *
931 maildir_get_subdir (char *filename)
932 {
933     char *p, *subdir = NULL;
934
935     p = filename + strlen (filename) - 1;
936     while (p > filename + 3 && *p != '/')
937         p--;
938     if (*p == '/') {
939         subdir = p - 3;
940         if (subdir > filename && *(subdir - 1) != '/')
941             subdir = NULL;
942     }
943     return subdir;
944 }
945
946 /* Rename the message file so that maildir flags corresponds to the
947  * tags and, if aplicable, move the message from new/ to cur/. */
948 static notmuch_private_status_t
949 _notmuch_message_tags_to_maildir (notmuch_message_t *message)
950 {
951     char flags[ARRAY_SIZE(flag2tag)+1];
952     const char *filename, *p;
953     char *filename_new, *subdir = NULL;
954     int ret;
955
956     maildir_get_new_flags (message, flags);
957
958     filename = notmuch_message_get_filename (message);
959     /* TODO: Iterate over all file names. */
960     p = strstr(filename, ":2,");
961     if ((p && strcmp (p+3, flags) == 0) ||
962         (!p && flags[0] == '\0')) {
963         // Return if flags are not to be changed - this suppresses
964         // moving the message from new/ to cur/ during initial
965         // tagging.
966         return NOTMUCH_PRIVATE_STATUS_SUCCESS;
967     }
968     if (!p)
969         p = filename + strlen(filename);
970
971     filename_new = (char*)talloc_size(message, (p-filename) + 3 + sizeof(flags));
972     if (unlikely (filename_new == NULL))
973         return NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
974     memcpy(filename_new, filename, p-filename);
975     filename_new[p-filename] = '\0';
976
977     /* If message is in new/ move it under cur/. */
978     subdir = maildir_get_subdir (filename_new);
979     if (subdir && memcmp (subdir, "new/", 4) == 0)
980         memcpy (subdir, "cur/", 4);
981
982     strcpy (filename_new+(p-filename), ":2,");
983     strcpy (filename_new+(p-filename)+3, flags);
984
985     if (strcmp (filename, filename_new) != 0) {
986         ret = rename (filename, filename_new);
987         if (ret == -1) {
988             perror (talloc_asprintf (message, "rename of %s to %s failed",
989                                      filename, filename_new));
990             exit (1);
991         }
992         return _notmuch_message_rename (message, filename_new);
993         /* _notmuch_message_sync is our caller. Do not call it here. */
994     }
995     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
996 }
997
998 notmuch_status_t
999 notmuch_message_remove_all_tags (notmuch_message_t *message)
1000 {
1001     notmuch_private_status_t private_status;
1002     notmuch_status_t status;
1003     notmuch_tags_t *tags;
1004     const char *tag;
1005
1006     status = _notmuch_database_ensure_writable (message->notmuch);
1007     if (status)
1008         return status;
1009
1010     for (tags = notmuch_message_get_tags (message);
1011          notmuch_tags_valid (tags);
1012          notmuch_tags_move_to_next (tags))
1013     {
1014         tag = notmuch_tags_get (tags);
1015
1016         private_status = _notmuch_message_remove_term (message, "tag", tag);
1017         if (private_status) {
1018             INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
1019                             private_status);
1020         }
1021     }
1022
1023     if (! message->frozen)
1024         _notmuch_message_sync (message);
1025
1026     return NOTMUCH_STATUS_SUCCESS;
1027 }
1028
1029 notmuch_status_t
1030 notmuch_message_freeze (notmuch_message_t *message)
1031 {
1032     notmuch_status_t status;
1033
1034     status = _notmuch_database_ensure_writable (message->notmuch);
1035     if (status)
1036         return status;
1037
1038     message->frozen++;
1039
1040     return NOTMUCH_STATUS_SUCCESS;
1041 }
1042
1043 notmuch_status_t
1044 notmuch_message_thaw (notmuch_message_t *message)
1045 {
1046     notmuch_status_t status;
1047
1048     status = _notmuch_database_ensure_writable (message->notmuch);
1049     if (status)
1050         return status;
1051
1052     if (message->frozen > 0) {
1053         message->frozen--;
1054         if (message->frozen == 0)
1055             _notmuch_message_sync (message);
1056         return NOTMUCH_STATUS_SUCCESS;
1057     } else {
1058         return NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW;
1059     }
1060 }
1061
1062 void
1063 notmuch_message_destroy (notmuch_message_t *message)
1064 {
1065     talloc_free (message);
1066 }