]> git.cworth.org Git - notmuch/blob - lib/message.cc
63a8da562b473aef926c9774ca99e9f0650f76f8
[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 https://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 visible _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 *tag_list;
36     notmuch_string_list_t *filename_term_list;
37     notmuch_string_list_t *filename_list;
38     char *author;
39     notmuch_message_file_t *message_file;
40     notmuch_string_list_t *property_term_list;
41     notmuch_string_map_t *property_map;
42     notmuch_message_list_t *replies;
43     unsigned long flags;
44     /* For flags that are initialized on-demand, lazy_flags indicates
45      * if each flag has been initialized. */
46     unsigned long lazy_flags;
47
48     /* Message document modified since last sync */
49     notmuch_bool_t modified;
50
51     Xapian::Document doc;
52     Xapian::termcount termpos;
53 };
54
55 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
56
57 struct maildir_flag_tag {
58     char flag;
59     const char *tag;
60     notmuch_bool_t inverse;
61 };
62
63 /* ASCII ordered table of Maildir flags and associated tags */
64 static struct maildir_flag_tag flag2tag[] = {
65     { 'D', "draft",   FALSE},
66     { 'F', "flagged", FALSE},
67     { 'P', "passed",  FALSE},
68     { 'R', "replied", FALSE},
69     { 'S', "unread",  TRUE }
70 };
71
72 /* We end up having to call the destructor explicitly because we had
73  * to use "placement new" in order to initialize C++ objects within a
74  * block that we allocated with talloc. So C++ is making talloc
75  * slightly less simple to use, (we wouldn't need
76  * talloc_set_destructor at all otherwise).
77  */
78 static int
79 _notmuch_message_destructor (notmuch_message_t *message)
80 {
81     message->doc.~Document ();
82
83     return 0;
84 }
85
86 static notmuch_message_t *
87 _notmuch_message_create_for_document (const void *talloc_owner,
88                                       notmuch_database_t *notmuch,
89                                       unsigned int doc_id,
90                                       Xapian::Document doc,
91                                       notmuch_private_status_t *status)
92 {
93     notmuch_message_t *message;
94
95     if (status)
96         *status = NOTMUCH_PRIVATE_STATUS_SUCCESS;
97
98     message = talloc (talloc_owner, notmuch_message_t);
99     if (unlikely (message == NULL)) {
100         if (status)
101             *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
102         return NULL;
103     }
104
105     message->notmuch = notmuch;
106     message->doc_id = doc_id;
107
108     message->frozen = 0;
109     message->flags = 0;
110     message->lazy_flags = 0;
111
112     /* Each of these will be lazily created as needed. */
113     message->message_id = NULL;
114     message->thread_id = NULL;
115     message->in_reply_to = NULL;
116     message->tag_list = NULL;
117     message->filename_term_list = NULL;
118     message->filename_list = NULL;
119     message->message_file = NULL;
120     message->author = NULL;
121     message->property_term_list = NULL;
122     message->property_map = NULL;
123
124     message->replies = _notmuch_message_list_create (message);
125     if (unlikely (message->replies == NULL)) {
126         if (status)
127             *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
128         return NULL;
129     }
130
131     /* This is C++'s creepy "placement new", which is really just an
132      * ugly way to call a constructor for a pre-allocated object. So
133      * it's really not an error to not be checking for OUT_OF_MEMORY
134      * here, since this "new" isn't actually allocating memory. This
135      * is language-design comedy of the wrong kind. */
136
137     new (&message->doc) Xapian::Document;
138
139     talloc_set_destructor (message, _notmuch_message_destructor);
140
141     message->doc = doc;
142     message->termpos = 0;
143
144     return message;
145 }
146
147 /* Create a new notmuch_message_t object for an existing document in
148  * the database.
149  *
150  * Here, 'talloc owner' is an optional talloc context to which the new
151  * message will belong. This allows for the caller to not bother
152  * calling notmuch_message_destroy on the message, and know that all
153  * memory will be reclaimed when 'talloc_owner' is freed. The caller
154  * still can call notmuch_message_destroy when finished with the
155  * message if desired.
156  *
157  * The 'talloc_owner' argument can also be NULL, in which case the
158  * caller *is* responsible for calling notmuch_message_destroy.
159  *
160  * If no document exists in the database with document ID of 'doc_id'
161  * then this function returns NULL and optionally sets *status to
162  * NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND.
163  *
164  * This function can also fail to due lack of available memory,
165  * returning NULL and optionally setting *status to
166  * NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY.
167  *
168  * The caller can pass NULL for status if uninterested in
169  * distinguishing these two cases.
170  */
171 notmuch_message_t *
172 _notmuch_message_create (const void *talloc_owner,
173                          notmuch_database_t *notmuch,
174                          unsigned int doc_id,
175                          notmuch_private_status_t *status)
176 {
177     Xapian::Document doc;
178
179     try {
180         doc = notmuch->xapian_db->get_document (doc_id);
181     } catch (const Xapian::DocNotFoundError &error) {
182         if (status)
183             *status = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
184         return NULL;
185     }
186
187     return _notmuch_message_create_for_document (talloc_owner, notmuch,
188                                                  doc_id, doc, status);
189 }
190
191 /* Create a new notmuch_message_t object for a specific message ID,
192  * (which may or may not already exist in the database).
193  *
194  * The 'notmuch' database will be the talloc owner of the returned
195  * message.
196  *
197  * This function returns a valid notmuch_message_t whether or not
198  * there is already a document in the database with the given message
199  * ID. These two cases can be distinguished by the value of *status:
200  *
201  *
202  *   NOTMUCH_PRIVATE_STATUS_SUCCESS:
203  *
204  *     There is already a document with message ID 'message_id' in the
205  *     database. The returned message can be used to query/modify the
206  *     document. The message may be a ghost message.
207  *
208  *   NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND:
209  *
210  *     No document with 'message_id' exists in the database. The
211  *     returned message contains a newly created document (not yet
212  *     added to the database) and a document ID that is known not to
213  *     exist in the database.  This message is "blank"; that is, it
214  *     contains only a message ID and no other metadata. The caller
215  *     can modify the message, and a call to _notmuch_message_sync
216  *     will add the document to the database.
217  *
218  * If an error occurs, this function will return NULL and *status
219  * will be set as appropriate. (The status pointer argument must
220  * not be NULL.)
221  */
222 notmuch_message_t *
223 _notmuch_message_create_for_message_id (notmuch_database_t *notmuch,
224                                         const char *message_id,
225                                         notmuch_private_status_t *status_ret)
226 {
227     notmuch_message_t *message;
228     Xapian::Document doc;
229     unsigned int doc_id;
230     char *term;
231
232     *status_ret = (notmuch_private_status_t) notmuch_database_find_message (notmuch,
233                                                                             message_id,
234                                                                             &message);
235     if (message)
236         return talloc_steal (notmuch, message);
237     else if (*status_ret)
238         return NULL;
239
240     /* If the message ID is too long, substitute its sha1 instead. */
241     if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX)
242         message_id = _notmuch_message_id_compressed (message, message_id);
243
244     term = talloc_asprintf (NULL, "%s%s",
245                             _find_prefix ("id"), message_id);
246     if (term == NULL) {
247         *status_ret = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
248         return NULL;
249     }
250
251     if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
252         INTERNAL_ERROR ("Failure to ensure database is writable.");
253
254     try {
255         doc.add_term (term, 0);
256         talloc_free (term);
257
258         doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id);
259
260         doc_id = _notmuch_database_generate_doc_id (notmuch);
261     } catch (const Xapian::Error &error) {
262         _notmuch_database_log(_notmuch_message_database (message), "A Xapian exception occurred creating message: %s\n",
263                  error.get_msg().c_str());
264         notmuch->exception_reported = TRUE;
265         *status_ret = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
266         return NULL;
267     }
268
269     message = _notmuch_message_create_for_document (notmuch, notmuch,
270                                                     doc_id, doc, status_ret);
271
272     /* We want to inform the caller that we had to create a new
273      * document. */
274     if (*status_ret == NOTMUCH_PRIVATE_STATUS_SUCCESS)
275         *status_ret = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
276
277     return message;
278 }
279
280 static char *
281 _notmuch_message_get_term (notmuch_message_t *message,
282                            Xapian::TermIterator &i, Xapian::TermIterator &end,
283                            const char *prefix)
284 {
285     int prefix_len = strlen (prefix);
286     char *value;
287
288     i.skip_to (prefix);
289
290     if (i == end)
291         return NULL;
292
293     const std::string &term = *i;
294     if (strncmp (term.c_str(), prefix, prefix_len))
295         return NULL;
296
297     value = talloc_strdup (message, term.c_str() + prefix_len);
298
299 #if DEBUG_DATABASE_SANITY
300     i++;
301
302     if (i != end && strncmp ((*i).c_str (), prefix, prefix_len) == 0) {
303         INTERNAL_ERROR ("Mail (doc_id: %d) has duplicate %s terms: %s and %s\n",
304                         message->doc_id, prefix, value,
305                         (*i).c_str () + prefix_len);
306     }
307 #endif
308
309     return value;
310 }
311
312 void
313 _notmuch_message_ensure_metadata (notmuch_message_t *message)
314 {
315     Xapian::TermIterator i, end;
316     const char *thread_prefix = _find_prefix ("thread"),
317         *tag_prefix = _find_prefix ("tag"),
318         *id_prefix = _find_prefix ("id"),
319         *type_prefix = _find_prefix ("type"),
320         *filename_prefix = _find_prefix ("file-direntry"),
321         *property_prefix = _find_prefix ("property"),
322         *replyto_prefix = _find_prefix ("replyto");
323
324     /* We do this all in a single pass because Xapian decompresses the
325      * term list every time you iterate over it.  Thus, while this is
326      * slightly more costly than looking up individual fields if only
327      * one field of the message object is actually used, it's a huge
328      * win as more fields are used. */
329
330     i = message->doc.termlist_begin ();
331     end = message->doc.termlist_end ();
332
333     /* Get thread */
334     if (!message->thread_id)
335         message->thread_id =
336             _notmuch_message_get_term (message, i, end, thread_prefix);
337
338     /* Get tags */
339     assert (strcmp (thread_prefix, tag_prefix) < 0);
340     if (!message->tag_list) {
341         message->tag_list =
342             _notmuch_database_get_terms_with_prefix (message, i, end,
343                                                      tag_prefix);
344         _notmuch_string_list_sort (message->tag_list);
345     }
346
347     /* Get id */
348     assert (strcmp (tag_prefix, id_prefix) < 0);
349     if (!message->message_id)
350         message->message_id =
351             _notmuch_message_get_term (message, i, end, id_prefix);
352
353     /* Get document type */
354     assert (strcmp (id_prefix, type_prefix) < 0);
355     if (! NOTMUCH_TEST_BIT (message->lazy_flags, NOTMUCH_MESSAGE_FLAG_GHOST)) {
356         i.skip_to (type_prefix);
357         /* "T" is the prefix "type" fields.  See
358          * BOOLEAN_PREFIX_INTERNAL. */
359         if (*i == "Tmail")
360             NOTMUCH_CLEAR_BIT (&message->flags, NOTMUCH_MESSAGE_FLAG_GHOST);
361         else if (*i == "Tghost")
362             NOTMUCH_SET_BIT (&message->flags, NOTMUCH_MESSAGE_FLAG_GHOST);
363         else
364             INTERNAL_ERROR ("Message without type term");
365         NOTMUCH_SET_BIT (&message->lazy_flags, NOTMUCH_MESSAGE_FLAG_GHOST);
366     }
367
368     /* Get filename list.  Here we get only the terms.  We lazily
369      * expand them to full file names when needed in
370      * _notmuch_message_ensure_filename_list. */
371     assert (strcmp (type_prefix, filename_prefix) < 0);
372     if (!message->filename_term_list && !message->filename_list)
373         message->filename_term_list =
374             _notmuch_database_get_terms_with_prefix (message, i, end,
375                                                      filename_prefix);
376
377
378     /* Get property terms. Mimic the setup with filenames above */
379     assert (strcmp (filename_prefix, property_prefix) < 0);
380     if (!message->property_map && !message->property_term_list)
381         message->property_term_list =
382             _notmuch_database_get_terms_with_prefix (message, i, end,
383                                                      property_prefix);
384
385     /* Get reply to */
386     assert (strcmp (property_prefix, replyto_prefix) < 0);
387     if (!message->in_reply_to)
388         message->in_reply_to =
389             _notmuch_message_get_term (message, i, end, replyto_prefix);
390
391
392     /* It's perfectly valid for a message to have no In-Reply-To
393      * header. For these cases, we return an empty string. */
394     if (!message->in_reply_to)
395         message->in_reply_to = talloc_strdup (message, "");
396 }
397
398 static void
399 _notmuch_message_invalidate_metadata (notmuch_message_t *message,
400                                       const char *prefix_name)
401 {
402     if (strcmp ("thread", prefix_name) == 0) {
403         talloc_free (message->thread_id);
404         message->thread_id = NULL;
405     }
406
407     if (strcmp ("tag", prefix_name) == 0) {
408         talloc_unlink (message, message->tag_list);
409         message->tag_list = NULL;
410     }
411
412     if (strcmp ("type", prefix_name) == 0) {
413         NOTMUCH_CLEAR_BIT (&message->flags, NOTMUCH_MESSAGE_FLAG_GHOST);
414         NOTMUCH_CLEAR_BIT (&message->lazy_flags, NOTMUCH_MESSAGE_FLAG_GHOST);
415     }
416
417     if (strcmp ("file-direntry", prefix_name) == 0) {
418         talloc_free (message->filename_term_list);
419         talloc_free (message->filename_list);
420         message->filename_term_list = message->filename_list = NULL;
421     }
422
423     if (strcmp ("property", prefix_name) == 0) {
424
425         if (message->property_term_list)
426             talloc_free (message->property_term_list);
427         message->property_term_list = NULL;
428
429         if (message->property_map)
430             talloc_unlink (message, message->property_map);
431
432         message->property_map = NULL;
433     }
434
435     if (strcmp ("replyto", prefix_name) == 0) {
436         talloc_free (message->in_reply_to);
437         message->in_reply_to = NULL;
438     }
439 }
440
441 unsigned int
442 _notmuch_message_get_doc_id (notmuch_message_t *message)
443 {
444     return message->doc_id;
445 }
446
447 const char *
448 notmuch_message_get_message_id (notmuch_message_t *message)
449 {
450     if (!message->message_id)
451         _notmuch_message_ensure_metadata (message);
452     if (!message->message_id)
453         INTERNAL_ERROR ("Message with document ID of %u has no message ID.\n",
454                         message->doc_id);
455     return message->message_id;
456 }
457
458 static void
459 _notmuch_message_ensure_message_file (notmuch_message_t *message)
460 {
461     const char *filename;
462
463     if (message->message_file)
464         return;
465
466     filename = notmuch_message_get_filename (message);
467     if (unlikely (filename == NULL))
468         return;
469
470     message->message_file = _notmuch_message_file_open_ctx (
471         _notmuch_message_database (message), message, filename);
472 }
473
474 const char *
475 notmuch_message_get_header (notmuch_message_t *message, const char *header)
476 {
477     Xapian::valueno slot = Xapian::BAD_VALUENO;
478
479     /* Fetch header from the appropriate xapian value field if
480      * available */
481     if (strcasecmp (header, "from") == 0)
482         slot = NOTMUCH_VALUE_FROM;
483     else if (strcasecmp (header, "subject") == 0)
484         slot = NOTMUCH_VALUE_SUBJECT;
485     else if (strcasecmp (header, "message-id") == 0)
486         slot = NOTMUCH_VALUE_MESSAGE_ID;
487
488     if (slot != Xapian::BAD_VALUENO) {
489         try {
490             std::string value = message->doc.get_value (slot);
491
492             /* If we have NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES, then
493              * empty values indicate empty headers.  If we don't, then
494              * it could just mean we didn't record the header. */
495             if ((message->notmuch->features &
496                  NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES) ||
497                 ! value.empty())
498                 return talloc_strdup (message, value.c_str ());
499
500         } catch (Xapian::Error &error) {
501             _notmuch_database_log(_notmuch_message_database (message), "A Xapian exception occurred when reading header: %s\n",
502                      error.get_msg().c_str());
503             message->notmuch->exception_reported = TRUE;
504             return NULL;
505         }
506     }
507
508     /* Otherwise fall back to parsing the file */
509     _notmuch_message_ensure_message_file (message);
510     if (message->message_file == NULL)
511         return NULL;
512
513     return _notmuch_message_file_get_header (message->message_file, header);
514 }
515
516 /* Return the message ID from the In-Reply-To header of 'message'.
517  *
518  * Returns an empty string ("") if 'message' has no In-Reply-To
519  * header.
520  *
521  * Returns NULL if any error occurs.
522  */
523 const char *
524 _notmuch_message_get_in_reply_to (notmuch_message_t *message)
525 {
526     if (!message->in_reply_to)
527         _notmuch_message_ensure_metadata (message);
528     return message->in_reply_to;
529 }
530
531 const char *
532 notmuch_message_get_thread_id (notmuch_message_t *message)
533 {
534     if (!message->thread_id)
535         _notmuch_message_ensure_metadata (message);
536     if (!message->thread_id)
537         INTERNAL_ERROR ("Message with document ID of %u has no thread ID.\n",
538                         message->doc_id);
539     return message->thread_id;
540 }
541
542 void
543 _notmuch_message_add_reply (notmuch_message_t *message,
544                             notmuch_message_t *reply)
545 {
546     _notmuch_message_list_add_message (message->replies, reply);
547 }
548
549 notmuch_messages_t *
550 notmuch_message_get_replies (notmuch_message_t *message)
551 {
552     return _notmuch_messages_create (message->replies);
553 }
554
555 static void
556 _notmuch_message_remove_terms (notmuch_message_t *message, const char *prefix)
557 {
558     Xapian::TermIterator i;
559     size_t prefix_len = strlen (prefix);
560
561     while (1) {
562         i = message->doc.termlist_begin ();
563         i.skip_to (prefix);
564
565         /* Terminate loop when no terms remain with desired prefix. */
566         if (i == message->doc.termlist_end () ||
567             strncmp ((*i).c_str (), prefix, prefix_len))
568             break;
569
570         try {
571             message->doc.remove_term ((*i));
572             message->modified = TRUE;
573         } catch (const Xapian::InvalidArgumentError) {
574             /* Ignore failure to remove non-existent term. */
575         }
576     }
577 }
578
579 /* Return true if p points at "new" or "cur". */
580 static bool is_maildir (const char *p)
581 {
582     return strcmp (p, "cur") == 0 || strcmp (p, "new") == 0;
583 }
584
585 /* Add "folder:" term for directory. */
586 static notmuch_status_t
587 _notmuch_message_add_folder_terms (notmuch_message_t *message,
588                                    const char *directory)
589 {
590     char *folder, *last;
591
592     folder = talloc_strdup (NULL, directory);
593     if (! folder)
594         return NOTMUCH_STATUS_OUT_OF_MEMORY;
595
596     /*
597      * If the message file is in a leaf directory named "new" or
598      * "cur", presume maildir and index the parent directory. Thus a
599      * "folder:" prefix search matches messages in the specified
600      * maildir folder, i.e. in the specified directory and its "new"
601      * and "cur" subdirectories.
602      *
603      * Note that this means the "folder:" prefix can't be used for
604      * distinguishing between message files in "new" or "cur". The
605      * "path:" prefix needs to be used for that.
606      *
607      * Note the deliberate difference to _filename_is_in_maildir(). We
608      * don't want to index different things depending on the existence
609      * or non-existence of all maildir sibling directories "new",
610      * "cur", and "tmp". Doing so would be surprising, and difficult
611      * for the user to fix in case all subdirectories were not in
612      * place during indexing.
613      */
614     last = strrchr (folder, '/');
615     if (last) {
616         if (is_maildir (last + 1))
617             *last = '\0';
618     } else if (is_maildir (folder)) {
619         *folder = '\0';
620     }
621
622     _notmuch_message_add_term (message, "folder", folder);
623
624     talloc_free (folder);
625
626     return NOTMUCH_STATUS_SUCCESS;
627 }
628
629 #define RECURSIVE_SUFFIX "/**"
630
631 /* Add "path:" terms for directory. */
632 static notmuch_status_t
633 _notmuch_message_add_path_terms (notmuch_message_t *message,
634                                  const char *directory)
635 {
636     /* Add exact "path:" term. */
637     _notmuch_message_add_term (message, "path", directory);
638
639     if (strlen (directory)) {
640         char *path, *p;
641
642         path = talloc_asprintf (NULL, "%s%s", directory, RECURSIVE_SUFFIX);
643         if (! path)
644             return NOTMUCH_STATUS_OUT_OF_MEMORY;
645
646         /* Add recursive "path:" terms for directory and all parents. */
647         for (p = path + strlen (path) - 1; p > path; p--) {
648             if (*p == '/') {
649                 strcpy (p, RECURSIVE_SUFFIX);
650                 _notmuch_message_add_term (message, "path", path);
651             }
652         }
653
654         talloc_free (path);
655     }
656
657     /* Recursive all-matching path:** for consistency. */
658     _notmuch_message_add_term (message, "path", "**");
659
660     return NOTMUCH_STATUS_SUCCESS;
661 }
662
663 /* Add directory based terms for all filenames of the message. */
664 static notmuch_status_t
665 _notmuch_message_add_directory_terms (void *ctx, notmuch_message_t *message)
666 {
667     const char *direntry_prefix = _find_prefix ("file-direntry");
668     int direntry_prefix_len = strlen (direntry_prefix);
669     Xapian::TermIterator i = message->doc.termlist_begin ();
670     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
671
672     for (i.skip_to (direntry_prefix); i != message->doc.termlist_end (); i++) {
673         unsigned int directory_id;
674         const char *direntry, *directory;
675         char *colon;
676         const std::string &term = *i;
677
678         /* Terminate loop at first term without desired prefix. */
679         if (strncmp (term.c_str (), direntry_prefix, direntry_prefix_len))
680             break;
681
682         /* Indicate that there are filenames remaining. */
683         status = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
684
685         direntry = term.c_str ();
686         direntry += direntry_prefix_len;
687
688         directory_id = strtol (direntry, &colon, 10);
689
690         if (colon == NULL || *colon != ':')
691             INTERNAL_ERROR ("malformed direntry");
692
693         directory = _notmuch_database_get_directory_path (ctx,
694                                                           message->notmuch,
695                                                           directory_id);
696
697         _notmuch_message_add_folder_terms (message, directory);
698         _notmuch_message_add_path_terms (message, directory);
699     }
700
701     return status;
702 }
703
704 /* Add an additional 'filename' for 'message'.
705  *
706  * This change will not be reflected in the database until the next
707  * call to _notmuch_message_sync. */
708 notmuch_status_t
709 _notmuch_message_add_filename (notmuch_message_t *message,
710                                const char *filename)
711 {
712     const char *relative, *directory;
713     notmuch_status_t status;
714     void *local = talloc_new (message);
715     char *direntry;
716
717     if (filename == NULL)
718         INTERNAL_ERROR ("Message filename cannot be NULL.");
719
720     if (! (message->notmuch->features & NOTMUCH_FEATURE_FILE_TERMS) ||
721         ! (message->notmuch->features & NOTMUCH_FEATURE_BOOL_FOLDER))
722         return NOTMUCH_STATUS_UPGRADE_REQUIRED;
723
724     relative = _notmuch_database_relative_path (message->notmuch, filename);
725
726     status = _notmuch_database_split_path (local, relative, &directory, NULL);
727     if (status)
728         return status;
729
730     status = _notmuch_database_filename_to_direntry (
731         local, message->notmuch, filename, NOTMUCH_FIND_CREATE, &direntry);
732     if (status)
733         return status;
734
735     /* New file-direntry allows navigating to this message with
736      * notmuch_directory_get_child_files() . */
737     _notmuch_message_add_term (message, "file-direntry", direntry);
738
739     _notmuch_message_add_folder_terms (message, directory);
740     _notmuch_message_add_path_terms (message, directory);
741
742     talloc_free (local);
743
744     return NOTMUCH_STATUS_SUCCESS;
745 }
746
747 /* Remove a particular 'filename' from 'message'.
748  *
749  * This change will not be reflected in the database until the next
750  * call to _notmuch_message_sync.
751  *
752  * If this message still has other filenames, returns
753  * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID.
754  *
755  * Note: This function does not remove a document from the database,
756  * even if the specified filename is the only filename for this
757  * message. For that functionality, see
758  * notmuch_database_remove_message. */
759 notmuch_status_t
760 _notmuch_message_remove_filename (notmuch_message_t *message,
761                                   const char *filename)
762 {
763     void *local = talloc_new (message);
764     char *direntry;
765     notmuch_private_status_t private_status;
766     notmuch_status_t status;
767
768     if (! (message->notmuch->features & NOTMUCH_FEATURE_FILE_TERMS) ||
769         ! (message->notmuch->features & NOTMUCH_FEATURE_BOOL_FOLDER))
770         return NOTMUCH_STATUS_UPGRADE_REQUIRED;
771
772     status = _notmuch_database_filename_to_direntry (
773         local, message->notmuch, filename, NOTMUCH_FIND_LOOKUP, &direntry);
774     if (status || !direntry)
775         return status;
776
777     /* Unlink this file from its parent directory. */
778     private_status = _notmuch_message_remove_term (message,
779                                                    "file-direntry", direntry);
780     status = COERCE_STATUS (private_status,
781                             "Unexpected error from _notmuch_message_remove_term");
782     if (status)
783         return status;
784
785     /* Re-synchronize "folder:" and "path:" terms for this message. */
786
787     /* Remove all "folder:" terms. */
788     _notmuch_message_remove_terms (message, _find_prefix ("folder"));
789
790     /* Remove all "path:" terms. */
791     _notmuch_message_remove_terms (message, _find_prefix ("path"));
792
793     /* Add back terms for all remaining filenames of the message. */
794     status = _notmuch_message_add_directory_terms (local, message);
795
796     talloc_free (local);
797
798     return status;
799 }
800
801 /* Upgrade the "folder:" prefix from V1 to V2. */
802 #define FOLDER_PREFIX_V1       "XFOLDER"
803 #define ZFOLDER_PREFIX_V1      "Z" FOLDER_PREFIX_V1
804 void
805 _notmuch_message_upgrade_folder (notmuch_message_t *message)
806 {
807     /* Remove all old "folder:" terms. */
808     _notmuch_message_remove_terms (message, FOLDER_PREFIX_V1);
809
810     /* Remove all old "folder:" stemmed terms. */
811     _notmuch_message_remove_terms (message, ZFOLDER_PREFIX_V1);
812
813     /* Add new boolean "folder:" and "path:" terms. */
814     _notmuch_message_add_directory_terms (message, message);
815 }
816
817 char *
818 _notmuch_message_talloc_copy_data (notmuch_message_t *message)
819 {
820     return talloc_strdup (message, message->doc.get_data ().c_str ());
821 }
822
823 void
824 _notmuch_message_clear_data (notmuch_message_t *message)
825 {
826     message->doc.set_data ("");
827     message->modified = TRUE;
828 }
829
830 static void
831 _notmuch_message_ensure_filename_list (notmuch_message_t *message)
832 {
833     notmuch_string_node_t *node;
834
835     if (message->filename_list)
836         return;
837
838     if (!message->filename_term_list)
839         _notmuch_message_ensure_metadata (message);
840
841     message->filename_list = _notmuch_string_list_create (message);
842     node = message->filename_term_list->head;
843
844     if (!node) {
845         /* A message document created by an old version of notmuch
846          * (prior to rename support) will have the filename in the
847          * data of the document rather than as a file-direntry term.
848          *
849          * It would be nice to do the upgrade of the document directly
850          * here, but the database is likely open in read-only mode. */
851         const char *data;
852
853         data = message->doc.get_data ().c_str ();
854
855         if (data == NULL)
856             INTERNAL_ERROR ("message with no filename");
857
858         _notmuch_string_list_append (message->filename_list, data);
859
860         return;
861     }
862
863     for (; node; node = node->next) {
864         void *local = talloc_new (message);
865         const char *db_path, *directory, *basename, *filename;
866         char *colon, *direntry = NULL;
867         unsigned int directory_id;
868
869         direntry = node->string;
870
871         directory_id = strtol (direntry, &colon, 10);
872
873         if (colon == NULL || *colon != ':')
874             INTERNAL_ERROR ("malformed direntry");
875
876         basename = colon + 1;
877
878         *colon = '\0';
879
880         db_path = notmuch_database_get_path (message->notmuch);
881
882         directory = _notmuch_database_get_directory_path (local,
883                                                           message->notmuch,
884                                                           directory_id);
885
886         if (strlen (directory))
887             filename = talloc_asprintf (message, "%s/%s/%s",
888                                         db_path, directory, basename);
889         else
890             filename = talloc_asprintf (message, "%s/%s",
891                                         db_path, basename);
892
893         _notmuch_string_list_append (message->filename_list, filename);
894
895         talloc_free (local);
896     }
897
898     talloc_free (message->filename_term_list);
899     message->filename_term_list = NULL;
900 }
901
902 const char *
903 notmuch_message_get_filename (notmuch_message_t *message)
904 {
905     _notmuch_message_ensure_filename_list (message);
906
907     if (message->filename_list == NULL)
908         return NULL;
909
910     if (message->filename_list->head == NULL ||
911         message->filename_list->head->string == NULL)
912     {
913         INTERNAL_ERROR ("message with no filename");
914     }
915
916     return message->filename_list->head->string;
917 }
918
919 notmuch_filenames_t *
920 notmuch_message_get_filenames (notmuch_message_t *message)
921 {
922     _notmuch_message_ensure_filename_list (message);
923
924     return _notmuch_filenames_create (message, message->filename_list);
925 }
926
927 notmuch_bool_t
928 notmuch_message_get_flag (notmuch_message_t *message,
929                           notmuch_message_flag_t flag)
930 {
931     if (flag == NOTMUCH_MESSAGE_FLAG_GHOST &&
932         ! NOTMUCH_TEST_BIT (message->lazy_flags, flag))
933         _notmuch_message_ensure_metadata (message);
934
935     return NOTMUCH_TEST_BIT (message->flags, flag);
936 }
937
938 void
939 notmuch_message_set_flag (notmuch_message_t *message,
940                           notmuch_message_flag_t flag, notmuch_bool_t enable)
941 {
942     if (enable)
943         NOTMUCH_SET_BIT (&message->flags, flag);
944     else
945         NOTMUCH_CLEAR_BIT (&message->flags, flag);
946     NOTMUCH_SET_BIT (&message->lazy_flags, flag);
947 }
948
949 time_t
950 notmuch_message_get_date (notmuch_message_t *message)
951 {
952     std::string value;
953
954     try {
955         value = message->doc.get_value (NOTMUCH_VALUE_TIMESTAMP);
956     } catch (Xapian::Error &error) {
957         _notmuch_database_log(_notmuch_message_database (message), "A Xapian exception occurred when reading date: %s\n",
958                  error.get_msg().c_str());
959         message->notmuch->exception_reported = TRUE;
960         return 0;
961     }
962
963     if (value.empty ())
964         /* sortable_unserialise is undefined on empty string */
965         return 0;
966     return Xapian::sortable_unserialise (value);
967 }
968
969 notmuch_tags_t *
970 notmuch_message_get_tags (notmuch_message_t *message)
971 {
972     notmuch_tags_t *tags;
973
974     if (!message->tag_list)
975         _notmuch_message_ensure_metadata (message);
976
977     tags = _notmuch_tags_create (message, message->tag_list);
978     /* _notmuch_tags_create steals the reference to the tag_list, but
979      * in this case it's still used by the message, so we add an
980      * *additional* talloc reference to the list.  As a result, it's
981      * possible to modify the message tags (which talloc_unlink's the
982      * current list from the message) while still iterating because
983      * the iterator will keep the current list alive. */
984     if (!talloc_reference (message, message->tag_list))
985         return NULL;
986
987     return tags;
988 }
989
990 const char *
991 _notmuch_message_get_author (notmuch_message_t *message)
992 {
993     return message->author;
994 }
995
996 void
997 _notmuch_message_set_author (notmuch_message_t *message,
998                             const char *author)
999 {
1000     if (message->author)
1001         talloc_free(message->author);
1002     message->author = talloc_strdup(message, author);
1003     return;
1004 }
1005
1006 void
1007 _notmuch_message_set_header_values (notmuch_message_t *message,
1008                                     const char *date,
1009                                     const char *from,
1010                                     const char *subject)
1011 {
1012     time_t time_value;
1013
1014     /* GMime really doesn't want to see a NULL date, so protect its
1015      * sensibilities. */
1016     if (date == NULL || *date == '\0')
1017         time_value = 0;
1018     else
1019         time_value = g_mime_utils_header_decode_date (date, NULL);
1020
1021     message->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
1022                             Xapian::sortable_serialise (time_value));
1023     message->doc.add_value (NOTMUCH_VALUE_FROM, from);
1024     message->doc.add_value (NOTMUCH_VALUE_SUBJECT, subject);
1025     message->modified = TRUE;
1026 }
1027
1028 /* Upgrade a message to support NOTMUCH_FEATURE_LAST_MOD.  The caller
1029  * must call _notmuch_message_sync. */
1030 void
1031 _notmuch_message_upgrade_last_mod (notmuch_message_t *message)
1032 {
1033     /* _notmuch_message_sync will update the last modification
1034      * revision; we just have to ask it to. */
1035     message->modified = TRUE;
1036 }
1037
1038 /* Synchronize changes made to message->doc out into the database. */
1039 void
1040 _notmuch_message_sync (notmuch_message_t *message)
1041 {
1042     Xapian::WritableDatabase *db;
1043
1044     if (message->notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
1045         return;
1046
1047     if (! message->modified)
1048         return;
1049
1050     /* Update the last modification of this message. */
1051     if (message->notmuch->features & NOTMUCH_FEATURE_LAST_MOD)
1052         /* sortable_serialise gives a reasonably compact encoding,
1053          * which directly translates to reduced IO when scanning the
1054          * value stream.  Since it's built for doubles, we only get 53
1055          * effective bits, but that's still enough for the database to
1056          * last a few centuries at 1 million revisions per second. */
1057         message->doc.add_value (NOTMUCH_VALUE_LAST_MOD,
1058                                 Xapian::sortable_serialise (
1059                                     _notmuch_database_new_revision (
1060                                         message->notmuch)));
1061
1062     db = static_cast <Xapian::WritableDatabase *> (message->notmuch->xapian_db);
1063     db->replace_document (message->doc_id, message->doc);
1064     message->modified = FALSE;
1065 }
1066
1067 /* Delete a message document from the database, leaving a ghost
1068  * message in its place */
1069 notmuch_status_t
1070 _notmuch_message_delete (notmuch_message_t *message)
1071 {
1072     notmuch_status_t status;
1073     Xapian::WritableDatabase *db;
1074     const char *mid, *tid, *query_string;
1075     notmuch_message_t *ghost;
1076     notmuch_private_status_t private_status;
1077     notmuch_database_t *notmuch;
1078     notmuch_query_t *query;
1079     unsigned int count = 0;
1080     notmuch_bool_t is_ghost;
1081
1082     mid = notmuch_message_get_message_id (message);
1083     tid = notmuch_message_get_thread_id (message);
1084     notmuch = message->notmuch;
1085
1086     status = _notmuch_database_ensure_writable (message->notmuch);
1087     if (status)
1088         return status;
1089
1090     db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
1091     db->delete_document (message->doc_id);
1092
1093     /* if this was a ghost to begin with, we are done */
1094     private_status = _notmuch_message_has_term (message, "type", "ghost", &is_ghost);
1095     if (private_status)
1096         return COERCE_STATUS (private_status,
1097                               "Error trying to determine whether message was a ghost");
1098     if (is_ghost)
1099         return NOTMUCH_STATUS_SUCCESS;
1100
1101     query_string = talloc_asprintf (message, "thread:%s", tid);
1102     query = notmuch_query_create (notmuch, query_string);
1103     if (query == NULL)
1104         return NOTMUCH_STATUS_OUT_OF_MEMORY;
1105     status = notmuch_query_count_messages_st (query, &count);
1106     if (status) {
1107         notmuch_query_destroy (query);
1108         return status;
1109     }
1110
1111     if (count > 0) {
1112         /* reintroduce a ghost in its place because there are still
1113          * other active messages in this thread: */
1114         ghost = _notmuch_message_create_for_message_id (notmuch, mid, &private_status);
1115         if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
1116             private_status = _notmuch_message_initialize_ghost (ghost, tid);
1117             if (! private_status)
1118                 _notmuch_message_sync (ghost);
1119         } else if (private_status == NOTMUCH_PRIVATE_STATUS_SUCCESS) {
1120             /* this is deeply weird, and we should not have gotten
1121                into this state.  is there a better error message to
1122                return here? */
1123             status = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
1124         }
1125
1126         notmuch_message_destroy (ghost);
1127         status = COERCE_STATUS (private_status, "Error converting to ghost message");
1128     } else {
1129         /* the thread is empty; drop all ghost messages from it */
1130         notmuch_messages_t *messages;
1131         status = _notmuch_query_search_documents (query,
1132                                                   "ghost",
1133                                                   &messages);
1134         if (status == NOTMUCH_STATUS_SUCCESS) {
1135             notmuch_status_t last_error = NOTMUCH_STATUS_SUCCESS;
1136             while (notmuch_messages_valid (messages)) {
1137                 message = notmuch_messages_get (messages);
1138                 status = _notmuch_message_delete (message);
1139                 if (status) /* we'll report the last failure we see;
1140                              * if there is more than one failure, we
1141                              * forget about previous ones */
1142                     last_error = status;
1143                 notmuch_message_destroy (message);
1144                 notmuch_messages_move_to_next (messages);
1145             }
1146             status = last_error;
1147         }
1148     }
1149     notmuch_query_destroy (query);
1150     return status;
1151 }
1152
1153 /* Transform a blank message into a ghost message.  The caller must
1154  * _notmuch_message_sync the message. */
1155 notmuch_private_status_t
1156 _notmuch_message_initialize_ghost (notmuch_message_t *message,
1157                                    const char *thread_id)
1158 {
1159     notmuch_private_status_t status;
1160
1161     status = _notmuch_message_add_term (message, "type", "ghost");
1162     if (status)
1163         return status;
1164     status = _notmuch_message_add_term (message, "thread", thread_id);
1165     if (status)
1166         return status;
1167
1168     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
1169 }
1170
1171 /* Ensure that 'message' is not holding any file object open. Future
1172  * calls to various functions will still automatically open the
1173  * message file as needed.
1174  */
1175 void
1176 _notmuch_message_close (notmuch_message_t *message)
1177 {
1178     if (message->message_file) {
1179         _notmuch_message_file_close (message->message_file);
1180         message->message_file = NULL;
1181     }
1182 }
1183
1184 /* Add a name:value term to 'message', (the actual term will be
1185  * encoded by prefixing the value with a short prefix). See
1186  * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
1187  * names to prefix values.
1188  *
1189  * This change will not be reflected in the database until the next
1190  * call to _notmuch_message_sync. */
1191 notmuch_private_status_t
1192 _notmuch_message_add_term (notmuch_message_t *message,
1193                            const char *prefix_name,
1194                            const char *value)
1195 {
1196
1197     char *term;
1198
1199     if (value == NULL)
1200         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
1201
1202     term = talloc_asprintf (message, "%s%s",
1203                             _find_prefix (prefix_name), value);
1204
1205     if (strlen (term) > NOTMUCH_TERM_MAX)
1206         return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
1207
1208     message->doc.add_term (term, 0);
1209     message->modified = TRUE;
1210
1211     talloc_free (term);
1212
1213     _notmuch_message_invalidate_metadata (message, prefix_name);
1214
1215     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
1216 }
1217
1218 /* Parse 'text' and add a term to 'message' for each parsed word. Each
1219  * term will be added both prefixed (if prefix_name is not NULL) and
1220  * also non-prefixed). */
1221 notmuch_private_status_t
1222 _notmuch_message_gen_terms (notmuch_message_t *message,
1223                             const char *prefix_name,
1224                             const char *text)
1225 {
1226     Xapian::TermGenerator *term_gen = message->notmuch->term_gen;
1227
1228     if (text == NULL)
1229         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
1230
1231     term_gen->set_document (message->doc);
1232
1233     if (prefix_name) {
1234         const char *prefix = _find_prefix (prefix_name);
1235
1236         term_gen->set_termpos (message->termpos);
1237         term_gen->index_text (text, 1, prefix);
1238         /* Create a gap between this an the next terms so they don't
1239          * appear to be a phrase. */
1240         message->termpos = term_gen->get_termpos () + 100;
1241
1242         _notmuch_message_invalidate_metadata (message, prefix_name);
1243     }
1244
1245     term_gen->set_termpos (message->termpos);
1246     term_gen->index_text (text);
1247     /* Create a term gap, as above. */
1248     message->termpos = term_gen->get_termpos () + 100;
1249
1250     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
1251 }
1252
1253 /* Remove a name:value term from 'message', (the actual term will be
1254  * encoded by prefixing the value with a short prefix). See
1255  * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
1256  * names to prefix values.
1257  *
1258  * This change will not be reflected in the database until the next
1259  * call to _notmuch_message_sync. */
1260 notmuch_private_status_t
1261 _notmuch_message_remove_term (notmuch_message_t *message,
1262                               const char *prefix_name,
1263                               const char *value)
1264 {
1265     char *term;
1266
1267     if (value == NULL)
1268         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
1269
1270     term = talloc_asprintf (message, "%s%s",
1271                             _find_prefix (prefix_name), value);
1272
1273     if (strlen (term) > NOTMUCH_TERM_MAX)
1274         return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
1275
1276     try {
1277         message->doc.remove_term (term);
1278         message->modified = TRUE;
1279     } catch (const Xapian::InvalidArgumentError) {
1280         /* We'll let the philosophers try to wrestle with the
1281          * question of whether failing to remove that which was not
1282          * there in the first place is failure. For us, we'll silently
1283          * consider it all good. */
1284     }
1285
1286     talloc_free (term);
1287
1288     _notmuch_message_invalidate_metadata (message, prefix_name);
1289
1290     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
1291 }
1292
1293 notmuch_private_status_t
1294 _notmuch_message_has_term (notmuch_message_t *message,
1295                            const char *prefix_name,
1296                            const char *value,
1297                            notmuch_bool_t *result)
1298 {
1299     char *term;
1300     notmuch_bool_t out = FALSE;
1301     notmuch_private_status_t status = NOTMUCH_PRIVATE_STATUS_SUCCESS;
1302
1303     if (value == NULL)
1304         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
1305
1306     term = talloc_asprintf (message, "%s%s",
1307                             _find_prefix (prefix_name), value);
1308
1309     if (strlen (term) > NOTMUCH_TERM_MAX)
1310         return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
1311
1312     try {
1313         /* Look for the exact term */
1314         Xapian::TermIterator i = message->doc.termlist_begin ();
1315         i.skip_to (term);
1316         if (i != message->doc.termlist_end () &&
1317             !strcmp ((*i).c_str (), term))
1318             out = TRUE;
1319     } catch (Xapian::Error &error) {
1320         status = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
1321     }
1322     talloc_free (term);
1323
1324     *result = out;
1325     return status;
1326 }
1327
1328 notmuch_status_t
1329 notmuch_message_add_tag (notmuch_message_t *message, const char *tag)
1330 {
1331     notmuch_private_status_t private_status;
1332     notmuch_status_t status;
1333
1334     status = _notmuch_database_ensure_writable (message->notmuch);
1335     if (status)
1336         return status;
1337
1338     if (tag == NULL)
1339         return NOTMUCH_STATUS_NULL_POINTER;
1340
1341     if (strlen (tag) > NOTMUCH_TAG_MAX)
1342         return NOTMUCH_STATUS_TAG_TOO_LONG;
1343
1344     private_status = _notmuch_message_add_term (message, "tag", tag);
1345     if (private_status) {
1346         INTERNAL_ERROR ("_notmuch_message_add_term return unexpected value: %d\n",
1347                         private_status);
1348     }
1349
1350     if (! message->frozen)
1351         _notmuch_message_sync (message);
1352
1353     return NOTMUCH_STATUS_SUCCESS;
1354 }
1355
1356 notmuch_status_t
1357 notmuch_message_remove_tag (notmuch_message_t *message, const char *tag)
1358 {
1359     notmuch_private_status_t private_status;
1360     notmuch_status_t status;
1361
1362     status = _notmuch_database_ensure_writable (message->notmuch);
1363     if (status)
1364         return status;
1365
1366     if (tag == NULL)
1367         return NOTMUCH_STATUS_NULL_POINTER;
1368
1369     if (strlen (tag) > NOTMUCH_TAG_MAX)
1370         return NOTMUCH_STATUS_TAG_TOO_LONG;
1371
1372     private_status = _notmuch_message_remove_term (message, "tag", tag);
1373     if (private_status) {
1374         INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
1375                         private_status);
1376     }
1377
1378     if (! message->frozen)
1379         _notmuch_message_sync (message);
1380
1381     return NOTMUCH_STATUS_SUCCESS;
1382 }
1383
1384 /* Is the given filename within a maildir directory?
1385  *
1386  * Specifically, is the final directory component of 'filename' either
1387  * "cur" or "new". If so, return a pointer to that final directory
1388  * component within 'filename'. If not, return NULL.
1389  *
1390  * A non-NULL return value is guaranteed to be a valid string pointer
1391  * pointing to the characters "new/" or "cur/", (but not
1392  * NUL-terminated).
1393  */
1394 static const char *
1395 _filename_is_in_maildir (const char *filename)
1396 {
1397     const char *slash, *dir = NULL;
1398
1399     /* Find the last '/' separating directory from filename. */
1400     slash = strrchr (filename, '/');
1401     if (slash == NULL)
1402         return NULL;
1403
1404     /* Jump back 4 characters to where the previous '/' will be if the
1405      * directory is named "cur" or "new". */
1406     if (slash - filename < 4)
1407         return NULL;
1408
1409     slash -= 4;
1410
1411     if (*slash != '/')
1412         return NULL;
1413
1414     dir = slash + 1;
1415
1416     if (STRNCMP_LITERAL (dir, "cur/") == 0 ||
1417         STRNCMP_LITERAL (dir, "new/") == 0)
1418     {
1419         return dir;
1420     }
1421
1422     return NULL;
1423 }
1424
1425 notmuch_status_t
1426 notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
1427 {
1428     const char *flags;
1429     notmuch_status_t status;
1430     notmuch_filenames_t *filenames;
1431     const char *filename, *dir;
1432     char *combined_flags = talloc_strdup (message, "");
1433     unsigned i;
1434     int seen_maildir_info = 0;
1435
1436     for (filenames = notmuch_message_get_filenames (message);
1437          notmuch_filenames_valid (filenames);
1438          notmuch_filenames_move_to_next (filenames))
1439     {
1440         filename = notmuch_filenames_get (filenames);
1441         dir = _filename_is_in_maildir (filename);
1442
1443         if (! dir)
1444             continue;
1445
1446         flags = strstr (filename, ":2,");
1447         if (flags) {
1448             seen_maildir_info = 1;
1449             flags += 3;
1450             combined_flags = talloc_strdup_append (combined_flags, flags);
1451         } else if (STRNCMP_LITERAL (dir, "new/") == 0) {
1452             /* Messages are delivered to new/ with no "info" part, but
1453              * they effectively have default maildir flags.  According
1454              * to the spec, we should ignore the info part for
1455              * messages in new/, but some MUAs (mutt) can set maildir
1456              * flags on messages in new/, so we're liberal in what we
1457              * accept. */
1458             seen_maildir_info = 1;
1459         }
1460     }
1461
1462     /* If none of the filenames have any maildir info field (not even
1463      * an empty info with no flags set) then there's no information to
1464      * go on, so do nothing. */
1465     if (! seen_maildir_info)
1466         return NOTMUCH_STATUS_SUCCESS;
1467
1468     status = notmuch_message_freeze (message);
1469     if (status)
1470         return status;
1471
1472     for (i = 0; i < ARRAY_SIZE(flag2tag); i++) {
1473         if ((strchr (combined_flags, flag2tag[i].flag) != NULL)
1474             ^
1475             flag2tag[i].inverse)
1476         {
1477             status = notmuch_message_add_tag (message, flag2tag[i].tag);
1478         } else {
1479             status = notmuch_message_remove_tag (message, flag2tag[i].tag);
1480         }
1481         if (status)
1482             return status;
1483     }
1484     status = notmuch_message_thaw (message);
1485
1486     talloc_free (combined_flags);
1487
1488     return status;
1489 }
1490
1491 /* From the set of tags on 'message' and the flag2tag table, compute a
1492  * set of maildir-flag actions to be taken, (flags that should be
1493  * either set or cleared).
1494  *
1495  * The result is returned as two talloced strings: to_set, and to_clear
1496  */
1497 static void
1498 _get_maildir_flag_actions (notmuch_message_t *message,
1499                            char **to_set_ret,
1500                            char **to_clear_ret)
1501 {
1502     char *to_set, *to_clear;
1503     notmuch_tags_t *tags;
1504     const char *tag;
1505     unsigned i;
1506
1507     to_set = talloc_strdup (message, "");
1508     to_clear = talloc_strdup (message, "");
1509
1510     /* First, find flags for all set tags. */
1511     for (tags = notmuch_message_get_tags (message);
1512          notmuch_tags_valid (tags);
1513          notmuch_tags_move_to_next (tags))
1514     {
1515         tag = notmuch_tags_get (tags);
1516
1517         for (i = 0; i < ARRAY_SIZE (flag2tag); i++) {
1518             if (strcmp (tag, flag2tag[i].tag) == 0) {
1519                 if (flag2tag[i].inverse)
1520                     to_clear = talloc_asprintf_append (to_clear,
1521                                                        "%c",
1522                                                        flag2tag[i].flag);
1523                 else
1524                     to_set = talloc_asprintf_append (to_set,
1525                                                      "%c",
1526                                                      flag2tag[i].flag);
1527             }
1528         }
1529     }
1530
1531     /* Then, find the flags for all tags not present. */
1532     for (i = 0; i < ARRAY_SIZE (flag2tag); i++) {
1533         if (flag2tag[i].inverse) {
1534             if (strchr (to_clear, flag2tag[i].flag) == NULL)
1535                 to_set = talloc_asprintf_append (to_set, "%c", flag2tag[i].flag);
1536         } else {
1537             if (strchr (to_set, flag2tag[i].flag) == NULL)
1538                 to_clear = talloc_asprintf_append (to_clear, "%c", flag2tag[i].flag);
1539         }
1540     }
1541
1542     *to_set_ret = to_set;
1543     *to_clear_ret = to_clear;
1544 }
1545
1546 /* Given 'filename' and a set of maildir flags to set and to clear,
1547  * compute the new maildir filename.
1548  *
1549  * If the existing filename is in the directory "new", the new
1550  * filename will be in the directory "cur", except for the case when
1551  * no flags are changed and the existing filename does not contain
1552  * maildir info (starting with ",2:").
1553  *
1554  * After a sequence of ":2," in the filename, any subsequent
1555  * single-character flags will be added or removed according to the
1556  * characters in flags_to_set and flags_to_clear. Any existing flags
1557  * not mentioned in either string will remain. The final list of flags
1558  * will be in ASCII order.
1559  *
1560  * If the original flags seem invalid, (repeated characters or
1561  * non-ASCII ordering of flags), this function will return NULL
1562  * (meaning that renaming would not be safe and should not occur).
1563  */
1564 static char*
1565 _new_maildir_filename (void *ctx,
1566                        const char *filename,
1567                        const char *flags_to_set,
1568                        const char *flags_to_clear)
1569 {
1570     const char *info, *flags;
1571     unsigned int flag, last_flag;
1572     char *filename_new, *dir;
1573     char flag_map[128];
1574     int flags_in_map = 0;
1575     notmuch_bool_t flags_changed = FALSE;
1576     unsigned int i;
1577     char *s;
1578
1579     memset (flag_map, 0, sizeof (flag_map));
1580
1581     info = strstr (filename, ":2,");
1582
1583     if (info == NULL) {
1584         info = filename + strlen(filename);
1585     } else {
1586         /* Loop through existing flags in filename. */
1587         for (flags = info + 3, last_flag = 0;
1588              *flags;
1589              last_flag = flag, flags++)
1590         {
1591             flag = *flags;
1592
1593             /* Original flags not in ASCII order. Abort. */
1594             if (flag < last_flag)
1595                 return NULL;
1596
1597             /* Non-ASCII flag. Abort. */
1598             if (flag > sizeof(flag_map) - 1)
1599                 return NULL;
1600
1601             /* Repeated flag value. Abort. */
1602             if (flag_map[flag])
1603                 return NULL;
1604
1605             flag_map[flag] = 1;
1606             flags_in_map++;
1607         }
1608     }
1609
1610     /* Then set and clear our flags from tags. */
1611     for (flags = flags_to_set; *flags; flags++) {
1612         flag = *flags;
1613         if (flag_map[flag] == 0) {
1614             flag_map[flag] = 1;
1615             flags_in_map++;
1616             flags_changed = TRUE;
1617         }
1618     }
1619
1620     for (flags = flags_to_clear; *flags; flags++) {
1621         flag = *flags;
1622         if (flag_map[flag]) {
1623             flag_map[flag] = 0;
1624             flags_in_map--;
1625             flags_changed = TRUE;
1626         }
1627     }
1628
1629     /* Messages in new/ without maildir info can be kept in new/ if no
1630      * flags have changed. */
1631     dir = (char *) _filename_is_in_maildir (filename);
1632     if (dir && STRNCMP_LITERAL (dir, "new/") == 0 && !*info && !flags_changed)
1633         return talloc_strdup (ctx, filename);
1634
1635     filename_new = (char *) talloc_size (ctx,
1636                                          info - filename +
1637                                          strlen (":2,") + flags_in_map + 1);
1638     if (unlikely (filename_new == NULL))
1639         return NULL;
1640
1641     strncpy (filename_new, filename, info - filename);
1642     filename_new[info - filename] = '\0';
1643
1644     strcat (filename_new, ":2,");
1645
1646     s = filename_new + strlen (filename_new);
1647     for (i = 0; i < sizeof (flag_map); i++)
1648     {
1649         if (flag_map[i]) {
1650             *s = i;
1651             s++;
1652         }
1653     }
1654     *s = '\0';
1655
1656     /* If message is in new/ move it under cur/. */
1657     dir = (char *) _filename_is_in_maildir (filename_new);
1658     if (dir && STRNCMP_LITERAL (dir, "new/") == 0)
1659         memcpy (dir, "cur/", 4);
1660
1661     return filename_new;
1662 }
1663
1664 notmuch_status_t
1665 notmuch_message_tags_to_maildir_flags (notmuch_message_t *message)
1666 {
1667     notmuch_filenames_t *filenames;
1668     const char *filename;
1669     char *filename_new;
1670     char *to_set, *to_clear;
1671     notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
1672
1673     _get_maildir_flag_actions (message, &to_set, &to_clear);
1674
1675     for (filenames = notmuch_message_get_filenames (message);
1676          notmuch_filenames_valid (filenames);
1677          notmuch_filenames_move_to_next (filenames))
1678     {
1679         filename = notmuch_filenames_get (filenames);
1680
1681         if (! _filename_is_in_maildir (filename))
1682             continue;
1683
1684         filename_new = _new_maildir_filename (message, filename,
1685                                               to_set, to_clear);
1686         if (filename_new == NULL)
1687             continue;
1688
1689         if (strcmp (filename, filename_new)) {
1690             int err;
1691             notmuch_status_t new_status;
1692
1693             err = rename (filename, filename_new);
1694             if (err)
1695                 continue;
1696
1697             new_status = _notmuch_message_remove_filename (message,
1698                                                            filename);
1699             /* Hold on to only the first error. */
1700             if (! status && new_status
1701                 && new_status != NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) {
1702                 status = new_status;
1703                 continue;
1704             }
1705
1706             new_status = _notmuch_message_add_filename (message,
1707                                                         filename_new);
1708             /* Hold on to only the first error. */
1709             if (! status && new_status) {
1710                 status = new_status;
1711                 continue;
1712             }
1713
1714             _notmuch_message_sync (message);
1715         }
1716
1717         talloc_free (filename_new);
1718     }
1719
1720     talloc_free (to_set);
1721     talloc_free (to_clear);
1722
1723     return status;
1724 }
1725
1726 notmuch_status_t
1727 notmuch_message_remove_all_tags (notmuch_message_t *message)
1728 {
1729     notmuch_private_status_t private_status;
1730     notmuch_status_t status;
1731     notmuch_tags_t *tags;
1732     const char *tag;
1733
1734     status = _notmuch_database_ensure_writable (message->notmuch);
1735     if (status)
1736         return status;
1737
1738     for (tags = notmuch_message_get_tags (message);
1739          notmuch_tags_valid (tags);
1740          notmuch_tags_move_to_next (tags))
1741     {
1742         tag = notmuch_tags_get (tags);
1743
1744         private_status = _notmuch_message_remove_term (message, "tag", tag);
1745         if (private_status) {
1746             INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
1747                             private_status);
1748         }
1749     }
1750
1751     if (! message->frozen)
1752         _notmuch_message_sync (message);
1753
1754     talloc_free (tags);
1755     return NOTMUCH_STATUS_SUCCESS;
1756 }
1757
1758 notmuch_status_t
1759 notmuch_message_freeze (notmuch_message_t *message)
1760 {
1761     notmuch_status_t status;
1762
1763     status = _notmuch_database_ensure_writable (message->notmuch);
1764     if (status)
1765         return status;
1766
1767     message->frozen++;
1768
1769     return NOTMUCH_STATUS_SUCCESS;
1770 }
1771
1772 notmuch_status_t
1773 notmuch_message_thaw (notmuch_message_t *message)
1774 {
1775     notmuch_status_t status;
1776
1777     status = _notmuch_database_ensure_writable (message->notmuch);
1778     if (status)
1779         return status;
1780
1781     if (message->frozen > 0) {
1782         message->frozen--;
1783         if (message->frozen == 0)
1784             _notmuch_message_sync (message);
1785         return NOTMUCH_STATUS_SUCCESS;
1786     } else {
1787         return NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW;
1788     }
1789 }
1790
1791 void
1792 notmuch_message_destroy (notmuch_message_t *message)
1793 {
1794     talloc_free (message);
1795 }
1796
1797 notmuch_database_t *
1798 _notmuch_message_database (notmuch_message_t *message)
1799 {
1800     return message->notmuch;
1801 }