]> git.notmuchmail.org Git - notmuch/blob - lib/message.cc
225b7e91bbaa4db27cd016fb587dd22c6e2e6523
[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     notmuch_filename_list_t *filename_list;
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 static 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 };
60
61 /* We end up having to call the destructor explicitly because we had
62  * to use "placement new" in order to initialize C++ objects within a
63  * block that we allocated with talloc. So C++ is making talloc
64  * slightly less simple to use, (we wouldn't need
65  * talloc_set_destructor at all otherwise).
66  */
67 static int
68 _notmuch_message_destructor (notmuch_message_t *message)
69 {
70     message->doc.~Document ();
71
72     return 0;
73 }
74
75 static notmuch_message_t *
76 _notmuch_message_create_for_document (const void *talloc_owner,
77                                       notmuch_database_t *notmuch,
78                                       unsigned int doc_id,
79                                       Xapian::Document doc,
80                                       notmuch_private_status_t *status)
81 {
82     notmuch_message_t *message;
83
84     if (status)
85         *status = NOTMUCH_PRIVATE_STATUS_SUCCESS;
86
87     message = talloc (talloc_owner, notmuch_message_t);
88     if (unlikely (message == NULL)) {
89         if (status)
90             *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
91         return NULL;
92     }
93
94     message->notmuch = notmuch;
95     message->doc_id = doc_id;
96
97     message->frozen = 0;
98     message->flags = 0;
99
100     /* Each of these will be lazily created as needed. */
101     message->message_id = NULL;
102     message->thread_id = NULL;
103     message->in_reply_to = NULL;
104     message->filename_list = NULL;
105     message->message_file = NULL;
106     message->author = NULL;
107
108     message->replies = _notmuch_message_list_create (message);
109     if (unlikely (message->replies == NULL)) {
110         if (status)
111             *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
112         return NULL;
113     }
114
115     /* This is C++'s creepy "placement new", which is really just an
116      * ugly way to call a constructor for a pre-allocated object. So
117      * it's really not an error to not be checking for OUT_OF_MEMORY
118      * here, since this "new" isn't actually allocating memory. This
119      * is language-design comedy of the wrong kind. */
120
121     new (&message->doc) Xapian::Document;
122
123     talloc_set_destructor (message, _notmuch_message_destructor);
124
125     message->doc = doc;
126
127     return message;
128 }
129
130 /* Create a new notmuch_message_t object for an existing document in
131  * the database.
132  *
133  * Here, 'talloc owner' is an optional talloc context to which the new
134  * message will belong. This allows for the caller to not bother
135  * calling notmuch_message_destroy on the message, and no that all
136  * memory will be reclaimed with 'talloc_owner' is free. The caller
137  * still can call notmuch_message_destroy when finished with the
138  * message if desired.
139  *
140  * The 'talloc_owner' argument can also be NULL, in which case the
141  * caller *is* responsible for calling notmuch_message_destroy.
142  *
143  * If no document exists in the database with document ID of 'doc_id'
144  * then this function returns NULL and optionally sets *status to
145  * NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND.
146  *
147  * This function can also fail to due lack of available memory,
148  * returning NULL and optionally setting *status to
149  * NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY.
150  *
151  * The caller can pass NULL for status if uninterested in
152  * distinguishing these two cases.
153  */
154 notmuch_message_t *
155 _notmuch_message_create (const void *talloc_owner,
156                          notmuch_database_t *notmuch,
157                          unsigned int doc_id,
158                          notmuch_private_status_t *status)
159 {
160     Xapian::Document doc;
161
162     try {
163         doc = notmuch->xapian_db->get_document (doc_id);
164     } catch (const Xapian::DocNotFoundError &error) {
165         if (status)
166             *status = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
167         return NULL;
168     }
169
170     return _notmuch_message_create_for_document (talloc_owner, notmuch,
171                                                  doc_id, doc, status);
172 }
173
174 /* Create a new notmuch_message_t object for a specific message ID,
175  * (which may or may not already exist in the database).
176  *
177  * The 'notmuch' database will be the talloc owner of the returned
178  * message.
179  *
180  * This function returns a valid notmuch_message_t whether or not
181  * there is already a document in the database with the given message
182  * ID. These two cases can be distinguished by the value of *status:
183  *
184  *
185  *   NOTMUCH_PRIVATE_STATUS_SUCCESS:
186  *
187  *     There is already a document with message ID 'message_id' in the
188  *     database. The returned message can be used to query/modify the
189  *     document.
190  *   NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND:
191  *
192  *     No document with 'message_id' exists in the database. The
193  *     returned message contains a newly created document (not yet
194  *     added to the database) and a document ID that is known not to
195  *     exist in the database. The caller can modify the message, and a
196  *     call to _notmuch_message_sync will add * the document to the
197  *     database.
198  *
199  * If an error occurs, this function will return NULL and *status
200  * will be set as appropriate. (The status pointer argument must
201  * not be NULL.)
202  */
203 notmuch_message_t *
204 _notmuch_message_create_for_message_id (notmuch_database_t *notmuch,
205                                         const char *message_id,
206                                         notmuch_private_status_t *status_ret)
207 {
208     notmuch_message_t *message;
209     Xapian::Document doc;
210     Xapian::WritableDatabase *db;
211     unsigned int doc_id;
212     char *term;
213
214     *status_ret = NOTMUCH_PRIVATE_STATUS_SUCCESS;
215
216     message = notmuch_database_find_message (notmuch, message_id);
217     if (message)
218         return talloc_steal (notmuch, message);
219
220     term = talloc_asprintf (NULL, "%s%s",
221                             _find_prefix ("id"), message_id);
222     if (term == NULL) {
223         *status_ret = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
224         return NULL;
225     }
226
227     if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
228         INTERNAL_ERROR ("Failure to ensure database is writable.");
229
230     db = static_cast<Xapian::WritableDatabase *> (notmuch->xapian_db);
231     try {
232         doc.add_term (term, 0);
233         talloc_free (term);
234
235         doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id);
236
237         doc_id = _notmuch_database_generate_doc_id (notmuch);
238     } catch (const Xapian::Error &error) {
239         fprintf (stderr, "A Xapian exception occurred creating message: %s\n",
240                  error.get_msg().c_str());
241         notmuch->exception_reported = TRUE;
242         *status_ret = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
243         return NULL;
244     }
245
246     message = _notmuch_message_create_for_document (notmuch, notmuch,
247                                                     doc_id, doc, status_ret);
248
249     /* We want to inform the caller that we had to create a new
250      * document. */
251     if (*status_ret == NOTMUCH_PRIVATE_STATUS_SUCCESS)
252         *status_ret = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
253
254     return message;
255 }
256
257 const char *
258 notmuch_message_get_message_id (notmuch_message_t *message)
259 {
260     Xapian::TermIterator i;
261
262     if (message->message_id)
263         return message->message_id;
264
265     i = message->doc.termlist_begin ();
266     i.skip_to (_find_prefix ("id"));
267
268     if (i == message->doc.termlist_end ())
269         INTERNAL_ERROR ("Message with document ID of %d has no message ID.\n",
270                         message->doc_id);
271
272     message->message_id = talloc_strdup (message, (*i).c_str () + 1);
273
274 #if DEBUG_DATABASE_SANITY
275     i++;
276
277     if (i != message->doc.termlist_end () &&
278         strncmp ((*i).c_str (), _find_prefix ("id"),
279                  strlen (_find_prefix ("id"))) == 0)
280     {
281         INTERNAL_ERROR ("Mail (doc_id: %d) has duplicate message IDs",
282                         message->doc_id);
283     }
284 #endif
285
286     return message->message_id;
287 }
288
289 static void
290 _notmuch_message_ensure_message_file (notmuch_message_t *message)
291 {
292     const char *filename;
293
294     if (message->message_file)
295         return;
296
297     filename = notmuch_message_get_filename (message);
298     if (unlikely (filename == NULL))
299         return;
300
301     message->message_file = _notmuch_message_file_open_ctx (message, filename);
302 }
303
304 const char *
305 notmuch_message_get_header (notmuch_message_t *message, const char *header)
306 {
307     _notmuch_message_ensure_message_file (message);
308     if (message->message_file == NULL)
309         return NULL;
310
311     return notmuch_message_file_get_header (message->message_file, header);
312 }
313
314 /* Return the message ID from the In-Reply-To header of 'message'.
315  *
316  * Returns an empty string ("") if 'message' has no In-Reply-To
317  * header.
318  *
319  * Returns NULL if any error occurs.
320  */
321 const char *
322 _notmuch_message_get_in_reply_to (notmuch_message_t *message)
323 {
324     const char *prefix = _find_prefix ("replyto");
325     int prefix_len = strlen (prefix);
326     Xapian::TermIterator i;
327     std::string in_reply_to;
328
329     if (message->in_reply_to)
330         return message->in_reply_to;
331
332     i = message->doc.termlist_begin ();
333     i.skip_to (prefix);
334
335     if (i != message->doc.termlist_end ())
336         in_reply_to = *i;
337
338     /* It's perfectly valid for a message to have no In-Reply-To
339      * header. For these cases, we return an empty string. */
340     if (i == message->doc.termlist_end () ||
341         strncmp (in_reply_to.c_str (), prefix, prefix_len))
342     {
343         message->in_reply_to = talloc_strdup (message, "");
344         return message->in_reply_to;
345     }
346
347     message->in_reply_to = talloc_strdup (message,
348                                           in_reply_to.c_str () + prefix_len);
349
350 #if DEBUG_DATABASE_SANITY
351     i++;
352
353     in_reply_to = *i;
354
355     if (i != message->doc.termlist_end () &&
356         strncmp ((*i).c_str (), prefix, prefix_len) == 0)
357     {
358        INTERNAL_ERROR ("Message %s has duplicate In-Reply-To IDs: %s and %s\n",
359                         notmuch_message_get_message_id (message),
360                         message->in_reply_to,
361                         (*i).c_str () + prefix_len);
362     }
363 #endif
364
365     return message->in_reply_to;
366 }
367
368 const char *
369 notmuch_message_get_thread_id (notmuch_message_t *message)
370 {
371     const char *prefix = _find_prefix ("thread");
372     Xapian::TermIterator i;
373     std::string id;
374
375     /* This code is written with the assumption that "thread" has a
376      * single-character prefix. */
377     assert (strlen (prefix) == 1);
378
379     if (message->thread_id)
380         return message->thread_id;
381
382     i = message->doc.termlist_begin ();
383     i.skip_to (prefix);
384
385     if (i != message->doc.termlist_end ())
386         id = *i;
387
388     if (i == message->doc.termlist_end () || id[0] != *prefix)
389         INTERNAL_ERROR ("Message with document ID of %d has no thread ID.\n",
390                         message->doc_id);
391
392     message->thread_id = talloc_strdup (message, id.c_str () + 1);
393
394 #if DEBUG_DATABASE_SANITY
395     i++;
396     id = *i;
397
398     if (i != message->doc.termlist_end () && id[0] == *prefix)
399     {
400         INTERNAL_ERROR ("Message %s has duplicate thread IDs: %s and %s\n",
401                         notmuch_message_get_message_id (message),
402                         message->thread_id,
403                         id.c_str () + 1);
404     }
405 #endif
406
407     return message->thread_id;
408 }
409
410 void
411 _notmuch_message_add_reply (notmuch_message_t *message,
412                             notmuch_message_node_t *reply)
413 {
414     _notmuch_message_list_append (message->replies, reply);
415 }
416
417 notmuch_messages_t *
418 notmuch_message_get_replies (notmuch_message_t *message)
419 {
420     return _notmuch_messages_create (message->replies);
421 }
422
423 /* Add an additional 'filename' for 'message'.
424  *
425  * This change will not be reflected in the database until the next
426  * call to _notmuch_message_sync. */
427 notmuch_status_t
428 _notmuch_message_add_filename (notmuch_message_t *message,
429                                const char *filename)
430 {
431     notmuch_status_t status;
432     void *local = talloc_new (message);
433     char *direntry;
434
435     if (message->filename_list) {
436         _notmuch_filename_list_destroy (message->filename_list);
437         message->filename_list = NULL;
438     }
439
440     if (filename == NULL)
441         INTERNAL_ERROR ("Message filename cannot be NULL.");
442
443     status = _notmuch_database_filename_to_direntry (local,
444                                                      message->notmuch,
445                                                      filename, &direntry);
446     if (status)
447         return status;
448
449     _notmuch_message_add_term (message, "file-direntry", direntry);
450
451     talloc_free (local);
452
453     return NOTMUCH_STATUS_SUCCESS;
454 }
455
456 /* Change a particular filename for 'message' from 'old_filename' to
457  * 'new_filename'
458  *
459  * This change will not be reflected in the database until the next
460  * call to _notmuch_message_sync.
461  */
462 static notmuch_status_t
463 _notmuch_message_rename (notmuch_message_t *message,
464                          const char *old_filename,
465                          const char *new_filename)
466 {
467     void *local = talloc_new (message);
468     char *direntry;
469     notmuch_private_status_t private_status;
470     notmuch_status_t status;
471
472     status = _notmuch_message_add_filename (message, new_filename);
473     if (status)
474         return status;
475
476     status = _notmuch_database_filename_to_direntry (local, message->notmuch,
477                                                      old_filename, &direntry);
478     if (status)
479         return status;
480
481     private_status = _notmuch_message_remove_term (message,
482                                                    "file-direntry", direntry);
483     status = COERCE_STATUS (private_status,
484                             "Unexpected error from _notmuch_message_remove_term");
485
486     talloc_free (local);
487
488     return status;
489 }
490
491 char *
492 _notmuch_message_talloc_copy_data (notmuch_message_t *message)
493 {
494     return talloc_strdup (message, message->doc.get_data ().c_str ());
495 }
496
497 void
498 _notmuch_message_clear_data (notmuch_message_t *message)
499 {
500     message->doc.set_data ("");
501 }
502
503 static void
504 _notmuch_message_ensure_filename_list (notmuch_message_t *message)
505 {
506     const char *prefix = _find_prefix ("file-direntry");
507     int prefix_len = strlen (prefix);
508     Xapian::TermIterator i;
509
510     if (message->filename_list)
511         return;
512
513     message->filename_list = _notmuch_filename_list_create (message);
514
515     i = message->doc.termlist_begin ();
516     i.skip_to (prefix);
517
518     if (i == message->doc.termlist_end () ||
519         strncmp ((*i).c_str (), prefix, prefix_len))
520     {
521         /* A message document created by an old version of notmuch
522          * (prior to rename support) will have the filename in the
523          * data of the document rather than as a file-direntry term.
524          *
525          * It would be nice to do the upgrade of the document directly
526          * here, but the database is likely open in read-only mode. */
527         const char *data;
528
529         data = message->doc.get_data ().c_str ();
530
531         if (data == NULL)
532             INTERNAL_ERROR ("message with no filename");
533
534         _notmuch_filename_list_add_filename (message->filename_list, data);
535
536         return;
537     }
538
539     for (; i != message->doc.termlist_end (); i++) {
540         void *local = talloc_new (message);
541         const char *db_path, *directory, *basename, *filename;
542         char *colon, *direntry = NULL;
543         unsigned int directory_id;
544
545         /* Terminate loop at first term without desired prefix. */
546         if (strncmp ((*i).c_str (), prefix, prefix_len))
547             break;
548
549         direntry = talloc_strdup (local, (*i).c_str ());
550
551         direntry += prefix_len;
552
553         directory_id = strtol (direntry, &colon, 10);
554
555         if (colon == NULL || *colon != ':')
556             INTERNAL_ERROR ("malformed direntry");
557
558         basename = colon + 1;
559
560         *colon = '\0';
561
562         db_path = notmuch_database_get_path (message->notmuch);
563
564         directory = _notmuch_database_get_directory_path (local,
565                                                           message->notmuch,
566                                                           directory_id);
567
568         if (strlen (directory))
569             filename = talloc_asprintf (message, "%s/%s/%s",
570                                         db_path, directory, basename);
571         else
572             filename = talloc_asprintf (message, "%s/%s",
573                                         db_path, basename);
574
575         _notmuch_filename_list_add_filename (message->filename_list,
576                                              filename);
577
578         talloc_free (local);
579     }
580 }
581
582 const char *
583 notmuch_message_get_filename (notmuch_message_t *message)
584 {
585     _notmuch_message_ensure_filename_list (message);
586
587     if (message->filename_list == NULL)
588         return NULL;
589
590     if (message->filename_list->head == NULL ||
591         message->filename_list->head->filename == NULL)
592     {
593         INTERNAL_ERROR ("message with no filename");
594     }
595
596     return message->filename_list->head->filename;
597 }
598
599 notmuch_filenames_t *
600 notmuch_message_get_filenames (notmuch_message_t *message)
601 {
602     _notmuch_message_ensure_filename_list (message);
603
604     return _notmuch_filenames_create (message, message->filename_list);
605 }
606
607 notmuch_bool_t
608 notmuch_message_get_flag (notmuch_message_t *message,
609                           notmuch_message_flag_t flag)
610 {
611     return message->flags & (1 << flag);
612 }
613
614 void
615 notmuch_message_set_flag (notmuch_message_t *message,
616                           notmuch_message_flag_t flag, notmuch_bool_t enable)
617 {
618     if (enable)
619         message->flags |= (1 << flag);
620     else
621         message->flags &= ~(1 << flag);
622 }
623
624 time_t
625 notmuch_message_get_date (notmuch_message_t *message)
626 {
627     std::string value;
628
629     try {
630         value = message->doc.get_value (NOTMUCH_VALUE_TIMESTAMP);
631     } catch (Xapian::Error &error) {
632         INTERNAL_ERROR ("Failed to read timestamp value from document.");
633         return 0;
634     }
635
636     return Xapian::sortable_unserialise (value);
637 }
638
639 notmuch_tags_t *
640 notmuch_message_get_tags (notmuch_message_t *message)
641 {
642     Xapian::TermIterator i, end;
643     i = message->doc.termlist_begin();
644     end = message->doc.termlist_end();
645     return _notmuch_convert_tags(message, i, end);
646 }
647
648 const char *
649 notmuch_message_get_author (notmuch_message_t *message)
650 {
651     return message->author;
652 }
653
654 void
655 notmuch_message_set_author (notmuch_message_t *message,
656                             const char *author)
657 {
658     if (message->author)
659         talloc_free(message->author);
660     message->author = talloc_strdup(message, author);
661     return;
662 }
663
664 void
665 _notmuch_message_set_date (notmuch_message_t *message,
666                            const char *date)
667 {
668     time_t time_value;
669
670     /* GMime really doesn't want to see a NULL date, so protect its
671      * sensibilities. */
672     if (date == NULL || *date == '\0')
673         time_value = 0;
674     else
675         time_value = g_mime_utils_header_decode_date (date, NULL);
676
677     message->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
678                             Xapian::sortable_serialise (time_value));
679 }
680
681 /* Synchronize changes made to message->doc out into the database. */
682 void
683 _notmuch_message_sync (notmuch_message_t *message)
684 {
685     Xapian::WritableDatabase *db;
686
687     if (message->notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
688         return;
689
690     db = static_cast <Xapian::WritableDatabase *> (message->notmuch->xapian_db);
691     db->replace_document (message->doc_id, message->doc);
692 }
693
694 /* Ensure that 'message' is not holding any file object open. Future
695  * calls to various functions will still automatically open the
696  * message file as needed.
697  */
698 void
699 _notmuch_message_close (notmuch_message_t *message)
700 {
701     if (message->message_file) {
702         notmuch_message_file_close (message->message_file);
703         message->message_file = NULL;
704     }
705 }
706
707 /* Add a name:value term to 'message', (the actual term will be
708  * encoded by prefixing the value with a short prefix). See
709  * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
710  * names to prefix values.
711  *
712  * This change will not be reflected in the database until the next
713  * call to _notmuch_message_sync. */
714 notmuch_private_status_t
715 _notmuch_message_add_term (notmuch_message_t *message,
716                            const char *prefix_name,
717                            const char *value)
718 {
719
720     char *term;
721
722     if (value == NULL)
723         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
724
725     term = talloc_asprintf (message, "%s%s",
726                             _find_prefix (prefix_name), value);
727
728     if (strlen (term) > NOTMUCH_TERM_MAX)
729         return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
730
731     message->doc.add_term (term, 0);
732
733     talloc_free (term);
734
735     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
736 }
737
738 /* Parse 'text' and add a term to 'message' for each parsed word. Each
739  * term will be added both prefixed (if prefix_name is not NULL) and
740  * also unprefixed). */
741 notmuch_private_status_t
742 _notmuch_message_gen_terms (notmuch_message_t *message,
743                             const char *prefix_name,
744                             const char *text)
745 {
746     Xapian::TermGenerator *term_gen = message->notmuch->term_gen;
747
748     if (text == NULL)
749         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
750
751     term_gen->set_document (message->doc);
752
753     if (prefix_name) {
754         const char *prefix = _find_prefix (prefix_name);
755
756         term_gen->index_text (text, 1, prefix);
757     }
758
759     term_gen->index_text (text);
760
761     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
762 }
763
764 /* Remove a name:value term from 'message', (the actual term will be
765  * encoded by prefixing the value with a short prefix). See
766  * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
767  * names to prefix values.
768  *
769  * This change will not be reflected in the database until the next
770  * call to _notmuch_message_sync. */
771 notmuch_private_status_t
772 _notmuch_message_remove_term (notmuch_message_t *message,
773                               const char *prefix_name,
774                               const char *value)
775 {
776     char *term;
777
778     if (value == NULL)
779         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
780
781     term = talloc_asprintf (message, "%s%s",
782                             _find_prefix (prefix_name), value);
783
784     if (strlen (term) > NOTMUCH_TERM_MAX)
785         return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
786
787     try {
788         message->doc.remove_term (term);
789     } catch (const Xapian::InvalidArgumentError) {
790         /* We'll let the philosopher's try to wrestle with the
791          * question of whether failing to remove that which was not
792          * there in the first place is failure. For us, we'll silently
793          * consider it all good. */
794     }
795
796     talloc_free (term);
797
798     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
799 }
800
801 notmuch_status_t
802 notmuch_message_add_tag (notmuch_message_t *message, const char *tag)
803 {
804     notmuch_private_status_t private_status;
805     notmuch_status_t status;
806
807     status = _notmuch_database_ensure_writable (message->notmuch);
808     if (status)
809         return status;
810
811     if (tag == NULL)
812         return NOTMUCH_STATUS_NULL_POINTER;
813
814     if (strlen (tag) > NOTMUCH_TAG_MAX)
815         return NOTMUCH_STATUS_TAG_TOO_LONG;
816
817     private_status = _notmuch_message_add_term (message, "tag", tag);
818     if (private_status) {
819         INTERNAL_ERROR ("_notmuch_message_add_term return unexpected value: %d\n",
820                         private_status);
821     }
822
823     if (! message->frozen)
824         _notmuch_message_sync (message);
825
826     return NOTMUCH_STATUS_SUCCESS;
827 }
828
829 notmuch_status_t
830 notmuch_message_remove_tag (notmuch_message_t *message, const char *tag)
831 {
832     notmuch_private_status_t private_status;
833     notmuch_status_t status;
834
835     status = _notmuch_database_ensure_writable (message->notmuch);
836     if (status)
837         return status;
838
839     if (tag == NULL)
840         return NOTMUCH_STATUS_NULL_POINTER;
841
842     if (strlen (tag) > NOTMUCH_TAG_MAX)
843         return NOTMUCH_STATUS_TAG_TOO_LONG;
844
845     private_status = _notmuch_message_remove_term (message, "tag", tag);
846     if (private_status) {
847         INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
848                         private_status);
849     }
850
851     if (! message->frozen)
852         _notmuch_message_sync (message);
853
854     return NOTMUCH_STATUS_SUCCESS;
855 }
856
857 notmuch_status_t
858 notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
859 {
860     const char *flags;
861     notmuch_status_t status;
862     notmuch_filenames_t *filenames;
863     const char *filename;
864     char *combined_flags = talloc_strdup (message, "");
865     unsigned i;
866     int seen_maildir_info = 0;
867
868     for (filenames = notmuch_message_get_filenames (message);
869          notmuch_filenames_valid (filenames);
870          notmuch_filenames_move_to_next (filenames))
871     {
872         filename = notmuch_filenames_get (filenames);
873
874         flags = strstr (filename, ":2,");
875         if (! flags)
876             continue;
877
878         seen_maildir_info = 1;
879         flags += 3;
880
881         combined_flags = talloc_strdup_append (combined_flags, flags);
882     }
883
884     /* If none of the filenames have any maildir info field (not even
885      * an empty info with no flags set) then there's no information to
886      * go on, so do nothing. */
887     if (! seen_maildir_info)
888         return NOTMUCH_STATUS_SUCCESS;
889
890     status = notmuch_message_freeze (message);
891     if (status)
892         return status;
893
894     for (i = 0; i < ARRAY_SIZE(flag2tag); i++) {
895         if ((strchr (combined_flags, flag2tag[i].flag) != NULL)
896             ^ 
897             flag2tag[i].inverse)
898         {
899             status = notmuch_message_add_tag (message, flag2tag[i].tag);
900         } else {
901             status = notmuch_message_remove_tag (message, flag2tag[i].tag);
902         }
903         if (status)
904             return status;
905     }
906     status = notmuch_message_thaw (message);
907
908     talloc_free (combined_flags);
909
910     return status;
911 }
912
913 /* Is the given filename within a maildir directory?
914  *
915  * Specifically, is the final directory component of 'filename' either
916  * "cur" or "new". If so, return a pointer to that final directory
917  * component within 'filename'. If not, return NULL.
918  *
919  * A non-NULL return value is guaranteed to be a valid string pointer
920  * pointing to the characters "new/" or "cur/", (but not
921  * NUL-terminated).
922  */
923 static const char *
924 _filename_is_in_maildir (const char *filename)
925 {
926     const char *slash, *dir = NULL;
927
928     /* Find the last '/' separating directory from filename. */
929     slash = strrchr (filename, '/');
930     if (slash == NULL)
931         return NULL;
932
933     /* Jump back 4 characters to where the previous '/' will be if the
934      * directory is named "cur" or "new". */
935     if (slash - filename < 4)
936         return NULL;
937
938     slash -= 4;
939
940     if (*slash != '/')
941         return NULL;
942
943     dir = slash + 1;
944
945     if (STRNCMP_LITERAL (dir, "cur/") == 0 ||
946         STRNCMP_LITERAL (dir, "new/") == 0)
947     {
948         return dir;
949     }
950
951     return NULL;
952 }
953
954 /* From the set of tags on 'message' and the flag2tag table, compute a
955  * set of maildir-flag actions to be taken, (flags that should be
956  * either set or cleared).
957  *
958  * The result is returned as two talloced strings: to_set, and to_clear
959  */
960 static void
961 _get_maildir_flag_actions (notmuch_message_t *message,
962                            char **to_set_ret,
963                            char **to_clear_ret)
964 {
965     char *to_set, *to_clear;
966     notmuch_tags_t *tags;
967     const char *tag;
968     unsigned i;
969
970     to_set = talloc_strdup (message, "");
971     to_clear = talloc_strdup (message, "");
972
973     /* First, find flags for all set tags. */
974     for (tags = notmuch_message_get_tags (message);
975          notmuch_tags_valid (tags);
976          notmuch_tags_move_to_next (tags))
977     {
978         tag = notmuch_tags_get (tags);
979
980         for (i = 0; i < ARRAY_SIZE (flag2tag); i++) {
981             if (strcmp (tag, flag2tag[i].tag) == 0) {
982                 if (flag2tag[i].inverse)
983                     to_clear = talloc_asprintf_append (to_clear,
984                                                        "%c",
985                                                        flag2tag[i].flag);
986                 else
987                     to_set = talloc_asprintf_append (to_set,
988                                                      "%c",
989                                                      flag2tag[i].flag);
990             }
991         }
992     }
993
994     /* Then, find the flags for all tags not present. */
995     for (i = 0; i < ARRAY_SIZE (flag2tag); i++) {
996         if (flag2tag[i].inverse) {
997             if (strchr (to_clear, flag2tag[i].flag) == NULL)
998                 to_set = talloc_asprintf_append (to_set, "%c", flag2tag[i].flag);
999         } else {
1000             if (strchr (to_set, flag2tag[i].flag) == NULL)
1001                 to_clear = talloc_asprintf_append (to_clear, "%c", flag2tag[i].flag);
1002         }
1003     }
1004
1005     *to_set_ret = to_set;
1006     *to_clear_ret = to_clear;
1007 }
1008
1009 /* Given 'filename' and a set of maildir flags to set and to clear,
1010  * compute the new maildir filename.
1011  *
1012  * If the existing filename is in the directory "new", the new
1013  * filename will be in the directory "cur".
1014  *
1015  * After a sequence of ":2," in the filename, any subsequent
1016  * single-character flags will be added or removed according to the
1017  * characters in flags_to_set and flags_to_clear. Any existing flags
1018  * not mentioned in either string will remain. The final list of flags
1019  * will be in ASCII order.
1020  *
1021  * If the original flags seem invalid, (repeated characters or
1022  * non-ASCII ordering of flags), this function will return NULL
1023  * (meaning that renaming would not be safe and should not occur).
1024  */
1025 static char*
1026 _new_maildir_filename (void *ctx,
1027                        const char *filename,
1028                        const char *flags_to_set,
1029                        const char *flags_to_clear)
1030 {
1031     const char *info, *flags;
1032     unsigned int flag, last_flag;
1033     char *filename_new, *dir;
1034     char flag_map[128];
1035     int flags_in_map = 0;
1036     unsigned int i;
1037     char *s;
1038
1039     memset (flag_map, 0, sizeof (flag_map));
1040
1041     info = strstr (filename, ":2,");
1042
1043     if (info == NULL) {
1044         info = filename + strlen(filename);
1045     } else {
1046         flags = info + 3;
1047
1048         /* Loop through existing flags in filename. */
1049         for (flags = info + 3, last_flag = 0;
1050              *flags;
1051              last_flag = flag, flags++)
1052         {
1053             flag = *flags;
1054
1055             /* Original flags not in ASCII order. Abort. */
1056             if (flag < last_flag)
1057                 return NULL;
1058
1059             /* Non-ASCII flag. Abort. */
1060             if (flag > sizeof(flag_map) - 1)
1061                 return NULL;
1062
1063             /* Repeated flag value. Abort. */
1064             if (flag_map[flag])
1065                 return NULL;
1066
1067             flag_map[flag] = 1;
1068             flags_in_map++;
1069         }
1070     }
1071
1072     /* Then set and clear our flags from tags. */
1073     for (flags = flags_to_set; *flags; flags++) {
1074         flag = *flags;
1075         if (flag_map[flag] == 0) {
1076             flag_map[flag] = 1;
1077             flags_in_map++;
1078         }
1079     }
1080
1081     for (flags = flags_to_clear; *flags; flags++) {
1082         flag = *flags;
1083         if (flag_map[flag]) {
1084             flag_map[flag] = 0;
1085             flags_in_map--;
1086         }
1087     }
1088
1089     filename_new = (char *) talloc_size (ctx,
1090                                          info - filename +
1091                                          strlen (":2,") + flags_in_map + 1);
1092     if (unlikely (filename_new == NULL))
1093         return NULL;
1094
1095     strncpy (filename_new, filename, info - filename);
1096     filename_new[info - filename] = '\0';
1097
1098     strcat (filename_new, ":2,");
1099
1100     s = filename_new + strlen (filename_new);
1101     for (i = 0; i < sizeof (flag_map); i++)
1102     {
1103         if (flag_map[i]) {
1104             *s = i;
1105             s++;
1106         }
1107     }
1108     *s = '\0';
1109
1110     /* If message is in new/ move it under cur/. */
1111     dir = (char *) _filename_is_in_maildir (filename_new);
1112     if (dir && STRNCMP_LITERAL (dir, "new/") == 0)
1113         memcpy (dir, "cur/", 4);
1114
1115     return filename_new;
1116 }
1117
1118 notmuch_status_t
1119 notmuch_message_tags_to_maildir_flags (notmuch_message_t *message)
1120 {
1121     notmuch_filenames_t *filenames;
1122     const char *filename;
1123     char *filename_new;
1124     char *to_set, *to_clear;
1125     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
1126
1127     _get_maildir_flag_actions (message, &to_set, &to_clear);
1128
1129     for (filenames = notmuch_message_get_filenames (message);
1130          notmuch_filenames_valid (filenames);
1131          notmuch_filenames_move_to_next (filenames))
1132     {
1133         filename = notmuch_filenames_get (filenames);
1134
1135         if (! _filename_is_in_maildir (filename))
1136             continue;
1137
1138         filename_new = _new_maildir_filename (message, filename,
1139                                               to_set, to_clear);
1140         if (filename_new == NULL)
1141             continue;
1142
1143         if (strcmp (filename, filename_new)) {
1144             int err;
1145             notmuch_status_t new_status;
1146
1147             err = rename (filename, filename_new);
1148             if (err)
1149                 continue;
1150
1151             new_status = _notmuch_message_rename (message,
1152                                                   filename, filename_new);
1153             /* Hold on to only the first error. */
1154             if (! status && new_status) {
1155                 status = new_status;
1156                 continue;
1157             }
1158
1159             _notmuch_message_sync (message);
1160         }
1161
1162         talloc_free (filename_new);
1163     }
1164
1165     talloc_free (to_set);
1166     talloc_free (to_clear);
1167
1168     return NOTMUCH_STATUS_SUCCESS;
1169 }
1170
1171 notmuch_status_t
1172 notmuch_message_remove_all_tags (notmuch_message_t *message)
1173 {
1174     notmuch_private_status_t private_status;
1175     notmuch_status_t status;
1176     notmuch_tags_t *tags;
1177     const char *tag;
1178
1179     status = _notmuch_database_ensure_writable (message->notmuch);
1180     if (status)
1181         return status;
1182
1183     for (tags = notmuch_message_get_tags (message);
1184          notmuch_tags_valid (tags);
1185          notmuch_tags_move_to_next (tags))
1186     {
1187         tag = notmuch_tags_get (tags);
1188
1189         private_status = _notmuch_message_remove_term (message, "tag", tag);
1190         if (private_status) {
1191             INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
1192                             private_status);
1193         }
1194     }
1195
1196     if (! message->frozen)
1197         _notmuch_message_sync (message);
1198
1199     return NOTMUCH_STATUS_SUCCESS;
1200 }
1201
1202 notmuch_status_t
1203 notmuch_message_freeze (notmuch_message_t *message)
1204 {
1205     notmuch_status_t status;
1206
1207     status = _notmuch_database_ensure_writable (message->notmuch);
1208     if (status)
1209         return status;
1210
1211     message->frozen++;
1212
1213     return NOTMUCH_STATUS_SUCCESS;
1214 }
1215
1216 notmuch_status_t
1217 notmuch_message_thaw (notmuch_message_t *message)
1218 {
1219     notmuch_status_t status;
1220
1221     status = _notmuch_database_ensure_writable (message->notmuch);
1222     if (status)
1223         return status;
1224
1225     if (message->frozen > 0) {
1226         message->frozen--;
1227         if (message->frozen == 0)
1228             _notmuch_message_sync (message);
1229         return NOTMUCH_STATUS_SUCCESS;
1230     } else {
1231         return NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW;
1232     }
1233 }
1234
1235 void
1236 notmuch_message_destroy (notmuch_message_t *message)
1237 {
1238     talloc_free (message);
1239 }