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