1 /* message.cc - Results of message-based searches from a notmuch database
3 * Copyright © 2009 Carl Worth
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.
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.
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/ .
18 * Author: Carl Worth <cworth@cworth.org>
21 #include "notmuch-private.h"
22 #include "database-private.h"
23 #include "message-private.h"
27 #include <gmime/gmime.h>
29 struct _notmuch_message {
30 notmuch_database_t *notmuch;
36 notmuch_string_list_t *tag_list;
37 notmuch_string_list_t *filename_term_list;
38 notmuch_string_list_t *filename_list;
41 notmuch_message_file_t *message_file;
42 notmuch_string_list_t *property_term_list;
43 notmuch_string_map_t *property_map;
44 notmuch_message_list_t *replies;
46 /* For flags that are initialized on-demand, lazy_flags indicates
47 * if each flag has been initialized. */
48 unsigned long lazy_flags;
50 /* Message document modified since last sync */
53 /* last view of database the struct is synced with */
54 unsigned long last_view;
57 Xapian::termcount termpos;
60 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
62 struct maildir_flag_tag {
68 /* ASCII ordered table of Maildir flags and associated tags */
69 static struct maildir_flag_tag flag2tag[] = {
70 { 'D', "draft", false},
71 { 'F', "flagged", false},
72 { 'P', "passed", false},
73 { 'R', "replied", false},
74 { 'S', "unread", true }
77 /* We end up having to call the destructor explicitly because we had
78 * to use "placement new" in order to initialize C++ objects within a
79 * block that we allocated with talloc. So C++ is making talloc
80 * slightly less simple to use, (we wouldn't need
81 * talloc_set_destructor at all otherwise).
84 _notmuch_message_destructor (notmuch_message_t *message)
86 message->doc.~Document ();
91 static notmuch_message_t *
92 _notmuch_message_create_for_document (const void *talloc_owner,
93 notmuch_database_t *notmuch,
96 notmuch_private_status_t *status)
98 notmuch_message_t *message;
101 *status = NOTMUCH_PRIVATE_STATUS_SUCCESS;
103 message = talloc (talloc_owner, notmuch_message_t);
104 if (unlikely (message == NULL)) {
106 *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
110 message->notmuch = notmuch;
111 message->doc_id = doc_id;
115 message->lazy_flags = 0;
117 /* the message is initially not synchronized with Xapian */
118 message->last_view = 0;
120 /* Each of these will be lazily created as needed. */
121 message->message_id = NULL;
122 message->thread_id = NULL;
123 message->in_reply_to = NULL;
124 message->tag_list = NULL;
125 message->filename_term_list = NULL;
126 message->filename_list = NULL;
127 message->maildir_flags = NULL;
128 message->message_file = NULL;
129 message->author = NULL;
130 message->property_term_list = NULL;
131 message->property_map = NULL;
133 message->replies = _notmuch_message_list_create (message);
134 if (unlikely (message->replies == NULL)) {
136 *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
140 /* This is C++'s creepy "placement new", which is really just an
141 * ugly way to call a constructor for a pre-allocated object. So
142 * it's really not an error to not be checking for OUT_OF_MEMORY
143 * here, since this "new" isn't actually allocating memory. This
144 * is language-design comedy of the wrong kind. */
146 new (&message->doc) Xapian::Document;
148 talloc_set_destructor (message, _notmuch_message_destructor);
151 message->termpos = 0;
156 /* Create a new notmuch_message_t object for an existing document in
159 * Here, 'talloc owner' is an optional talloc context to which the new
160 * message will belong. This allows for the caller to not bother
161 * calling notmuch_message_destroy on the message, and know that all
162 * memory will be reclaimed when 'talloc_owner' is freed. The caller
163 * still can call notmuch_message_destroy when finished with the
164 * message if desired.
166 * The 'talloc_owner' argument can also be NULL, in which case the
167 * caller *is* responsible for calling notmuch_message_destroy.
169 * If no document exists in the database with document ID of 'doc_id'
170 * then this function returns NULL and optionally sets *status to
171 * NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND.
173 * This function can also fail to due lack of available memory,
174 * returning NULL and optionally setting *status to
175 * NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY.
177 * The caller can pass NULL for status if uninterested in
178 * distinguishing these two cases.
181 _notmuch_message_create (const void *talloc_owner,
182 notmuch_database_t *notmuch,
184 notmuch_private_status_t *status)
186 Xapian::Document doc;
189 doc = notmuch->xapian_db->get_document (doc_id);
190 } catch (const Xapian::DocNotFoundError &error) {
192 *status = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
196 return _notmuch_message_create_for_document (talloc_owner, notmuch,
197 doc_id, doc, status);
200 /* Create a new notmuch_message_t object for a specific message ID,
201 * (which may or may not already exist in the database).
203 * The 'notmuch' database will be the talloc owner of the returned
206 * This function returns a valid notmuch_message_t whether or not
207 * there is already a document in the database with the given message
208 * ID. These two cases can be distinguished by the value of *status:
211 * NOTMUCH_PRIVATE_STATUS_SUCCESS:
213 * There is already a document with message ID 'message_id' in the
214 * database. The returned message can be used to query/modify the
215 * document. The message may be a ghost message.
217 * NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND:
219 * No document with 'message_id' exists in the database. The
220 * returned message contains a newly created document (not yet
221 * added to the database) and a document ID that is known not to
222 * exist in the database. This message is "blank"; that is, it
223 * contains only a message ID and no other metadata. The caller
224 * can modify the message, and a call to _notmuch_message_sync
225 * will add the document to the database.
227 * If an error occurs, this function will return NULL and *status
228 * will be set as appropriate. (The status pointer argument must
232 _notmuch_message_create_for_message_id (notmuch_database_t *notmuch,
233 const char *message_id,
234 notmuch_private_status_t *status_ret)
236 notmuch_message_t *message;
237 Xapian::Document doc;
241 *status_ret = (notmuch_private_status_t) notmuch_database_find_message (notmuch,
245 return talloc_steal (notmuch, message);
246 else if (*status_ret)
249 /* If the message ID is too long, substitute its sha1 instead. */
250 if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX)
251 message_id = _notmuch_message_id_compressed (message, message_id);
253 term = talloc_asprintf (NULL, "%s%s",
254 _find_prefix ("id"), message_id);
256 *status_ret = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
260 if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
261 INTERNAL_ERROR ("Failure to ensure database is writable.");
264 doc.add_term (term, 0);
267 doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id);
269 doc_id = _notmuch_database_generate_doc_id (notmuch);
270 } catch (const Xapian::Error &error) {
271 _notmuch_database_log(_notmuch_message_database (message), "A Xapian exception occurred creating message: %s\n",
272 error.get_msg().c_str());
273 notmuch->exception_reported = true;
274 *status_ret = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
278 message = _notmuch_message_create_for_document (notmuch, notmuch,
279 doc_id, doc, status_ret);
281 /* We want to inform the caller that we had to create a new
283 if (*status_ret == NOTMUCH_PRIVATE_STATUS_SUCCESS)
284 *status_ret = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
290 _notmuch_message_get_term (notmuch_message_t *message,
291 Xapian::TermIterator &i, Xapian::TermIterator &end,
294 int prefix_len = strlen (prefix);
302 const std::string &term = *i;
303 if (strncmp (term.c_str(), prefix, prefix_len))
306 value = talloc_strdup (message, term.c_str() + prefix_len);
308 #if DEBUG_DATABASE_SANITY
311 if (i != end && strncmp ((*i).c_str (), prefix, prefix_len) == 0) {
312 INTERNAL_ERROR ("Mail (doc_id: %d) has duplicate %s terms: %s and %s\n",
313 message->doc_id, prefix, value,
314 (*i).c_str () + prefix_len);
322 _notmuch_message_ensure_metadata (notmuch_message_t *message, void *field)
324 Xapian::TermIterator i, end;
326 if (field && (message->last_view >= message->notmuch->view))
329 const char *thread_prefix = _find_prefix ("thread"),
330 *tag_prefix = _find_prefix ("tag"),
331 *id_prefix = _find_prefix ("id"),
332 *type_prefix = _find_prefix ("type"),
333 *filename_prefix = _find_prefix ("file-direntry"),
334 *property_prefix = _find_prefix ("property"),
335 *replyto_prefix = _find_prefix ("replyto");
337 /* We do this all in a single pass because Xapian decompresses the
338 * term list every time you iterate over it. Thus, while this is
339 * slightly more costly than looking up individual fields if only
340 * one field of the message object is actually used, it's a huge
341 * win as more fields are used. */
342 for (int count=0; count < 3; count++) {
344 i = message->doc.termlist_begin ();
345 end = message->doc.termlist_end ();
348 if (!message->thread_id)
350 _notmuch_message_get_term (message, i, end, thread_prefix);
353 assert (strcmp (thread_prefix, tag_prefix) < 0);
354 if (!message->tag_list) {
356 _notmuch_database_get_terms_with_prefix (message, i, end,
358 _notmuch_string_list_sort (message->tag_list);
362 assert (strcmp (tag_prefix, id_prefix) < 0);
363 if (!message->message_id)
364 message->message_id =
365 _notmuch_message_get_term (message, i, end, id_prefix);
367 /* Get document type */
368 assert (strcmp (id_prefix, type_prefix) < 0);
369 if (! NOTMUCH_TEST_BIT (message->lazy_flags, NOTMUCH_MESSAGE_FLAG_GHOST)) {
370 i.skip_to (type_prefix);
371 /* "T" is the prefix "type" fields. See
372 * BOOLEAN_PREFIX_INTERNAL. */
374 NOTMUCH_CLEAR_BIT (&message->flags, NOTMUCH_MESSAGE_FLAG_GHOST);
375 else if (*i == "Tghost")
376 NOTMUCH_SET_BIT (&message->flags, NOTMUCH_MESSAGE_FLAG_GHOST);
378 INTERNAL_ERROR ("Message without type term");
379 NOTMUCH_SET_BIT (&message->lazy_flags, NOTMUCH_MESSAGE_FLAG_GHOST);
382 /* Get filename list. Here we get only the terms. We lazily
383 * expand them to full file names when needed in
384 * _notmuch_message_ensure_filename_list. */
385 assert (strcmp (type_prefix, filename_prefix) < 0);
386 if (!message->filename_term_list && !message->filename_list)
387 message->filename_term_list =
388 _notmuch_database_get_terms_with_prefix (message, i, end,
392 /* Get property terms. Mimic the setup with filenames above */
393 assert (strcmp (filename_prefix, property_prefix) < 0);
394 if (!message->property_map && !message->property_term_list)
395 message->property_term_list =
396 _notmuch_database_get_terms_with_prefix (message, i, end,
400 assert (strcmp (property_prefix, replyto_prefix) < 0);
401 if (!message->in_reply_to)
402 message->in_reply_to =
403 _notmuch_message_get_term (message, i, end, replyto_prefix);
406 /* It's perfectly valid for a message to have no In-Reply-To
407 * header. For these cases, we return an empty string. */
408 if (!message->in_reply_to)
409 message->in_reply_to = talloc_strdup (message, "");
411 /* all the way without an exception */
413 } catch (const Xapian::DatabaseModifiedError &error) {
414 notmuch_status_t status = _notmuch_database_reopen (message->notmuch);
415 if (status != NOTMUCH_STATUS_SUCCESS)
416 INTERNAL_ERROR ("unhandled error from notmuch_database_reopen: %s\n",
417 notmuch_status_to_string (status));
418 } catch (const Xapian::Error &error) {
419 INTERNAL_ERROR ("A Xapian exception occurred fetching message metadata: %s\n",
420 error.get_msg().c_str());
423 message->last_view = message->notmuch->view;
427 _notmuch_message_invalidate_metadata (notmuch_message_t *message,
428 const char *prefix_name)
430 if (strcmp ("thread", prefix_name) == 0) {
431 talloc_free (message->thread_id);
432 message->thread_id = NULL;
435 if (strcmp ("tag", prefix_name) == 0) {
436 talloc_unlink (message, message->tag_list);
437 message->tag_list = NULL;
440 if (strcmp ("type", prefix_name) == 0) {
441 NOTMUCH_CLEAR_BIT (&message->flags, NOTMUCH_MESSAGE_FLAG_GHOST);
442 NOTMUCH_CLEAR_BIT (&message->lazy_flags, NOTMUCH_MESSAGE_FLAG_GHOST);
445 if (strcmp ("file-direntry", prefix_name) == 0) {
446 talloc_free (message->filename_term_list);
447 talloc_free (message->filename_list);
448 message->filename_term_list = message->filename_list = NULL;
451 if (strcmp ("property", prefix_name) == 0) {
453 if (message->property_term_list)
454 talloc_free (message->property_term_list);
455 message->property_term_list = NULL;
457 if (message->property_map)
458 talloc_unlink (message, message->property_map);
460 message->property_map = NULL;
463 if (strcmp ("replyto", prefix_name) == 0) {
464 talloc_free (message->in_reply_to);
465 message->in_reply_to = NULL;
470 _notmuch_message_get_doc_id (notmuch_message_t *message)
472 return message->doc_id;
476 notmuch_message_get_message_id (notmuch_message_t *message)
478 _notmuch_message_ensure_metadata (message, message->message_id);
479 if (!message->message_id)
480 INTERNAL_ERROR ("Message with document ID of %u has no message ID.\n",
482 return message->message_id;
486 _notmuch_message_ensure_message_file (notmuch_message_t *message)
488 const char *filename;
490 if (message->message_file)
493 filename = notmuch_message_get_filename (message);
494 if (unlikely (filename == NULL))
497 message->message_file = _notmuch_message_file_open_ctx (
498 _notmuch_message_database (message), message, filename);
502 notmuch_message_get_header (notmuch_message_t *message, const char *header)
504 Xapian::valueno slot = Xapian::BAD_VALUENO;
506 /* Fetch header from the appropriate xapian value field if
508 if (strcasecmp (header, "from") == 0)
509 slot = NOTMUCH_VALUE_FROM;
510 else if (strcasecmp (header, "subject") == 0)
511 slot = NOTMUCH_VALUE_SUBJECT;
512 else if (strcasecmp (header, "message-id") == 0)
513 slot = NOTMUCH_VALUE_MESSAGE_ID;
515 if (slot != Xapian::BAD_VALUENO) {
517 std::string value = message->doc.get_value (slot);
519 /* If we have NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES, then
520 * empty values indicate empty headers. If we don't, then
521 * it could just mean we didn't record the header. */
522 if ((message->notmuch->features &
523 NOTMUCH_FEATURE_FROM_SUBJECT_ID_VALUES) ||
525 return talloc_strdup (message, value.c_str ());
527 } catch (Xapian::Error &error) {
528 _notmuch_database_log(_notmuch_message_database (message), "A Xapian exception occurred when reading header: %s\n",
529 error.get_msg().c_str());
530 message->notmuch->exception_reported = true;
535 /* Otherwise fall back to parsing the file */
536 _notmuch_message_ensure_message_file (message);
537 if (message->message_file == NULL)
540 return _notmuch_message_file_get_header (message->message_file, header);
543 /* Return the message ID from the In-Reply-To header of 'message'.
545 * Returns an empty string ("") if 'message' has no In-Reply-To
548 * Returns NULL if any error occurs.
551 _notmuch_message_get_in_reply_to (notmuch_message_t *message)
553 _notmuch_message_ensure_metadata (message, message->in_reply_to);
554 return message->in_reply_to;
558 notmuch_message_get_thread_id (notmuch_message_t *message)
560 _notmuch_message_ensure_metadata (message, message->thread_id);
561 if (!message->thread_id)
562 INTERNAL_ERROR ("Message with document ID of %u has no thread ID.\n",
564 return message->thread_id;
568 _notmuch_message_add_reply (notmuch_message_t *message,
569 notmuch_message_t *reply)
571 _notmuch_message_list_add_message (message->replies, reply);
575 notmuch_message_get_replies (notmuch_message_t *message)
577 return _notmuch_messages_create (message->replies);
581 _notmuch_message_remove_terms (notmuch_message_t *message, const char *prefix)
583 Xapian::TermIterator i;
584 size_t prefix_len = 0;
586 prefix_len = strlen (prefix);
589 i = message->doc.termlist_begin ();
592 /* Terminate loop when no terms remain with desired prefix. */
593 if (i == message->doc.termlist_end () ||
594 strncmp ((*i).c_str (), prefix, prefix_len))
598 message->doc.remove_term ((*i));
599 message->modified = true;
600 } catch (const Xapian::InvalidArgumentError) {
601 /* Ignore failure to remove non-existent term. */
607 /* Remove all terms generated by indexing, i.e. not tags or
608 * properties, along with any automatic tags*/
609 notmuch_private_status_t
610 _notmuch_message_remove_indexed_terms (notmuch_message_t *message)
612 Xapian::TermIterator i;
615 id_prefix = _find_prefix ("id"),
616 property_prefix = _find_prefix ("property"),
617 tag_prefix = _find_prefix ("tag"),
618 type_prefix = _find_prefix ("type");
620 for (i = message->doc.termlist_begin ();
621 i != message->doc.termlist_end (); i++) {
623 const std::string term = *i;
625 if (term.compare (0, type_prefix.size (), type_prefix) == 0)
628 if (term.compare (0, id_prefix.size (), id_prefix) == 0)
631 if (term.compare (0, property_prefix.size (), property_prefix) == 0)
634 if (term.compare (0, tag_prefix.size (), tag_prefix) == 0 &&
635 term.compare (1, strlen("encrypted"), "encrypted") != 0 &&
636 term.compare (1, strlen("signed"), "signed") != 0 &&
637 term.compare (1, strlen("attachment"), "attachment") != 0)
641 message->doc.remove_term ((*i));
642 message->modified = true;
643 } catch (const Xapian::InvalidArgumentError) {
644 /* Ignore failure to remove non-existent term. */
645 } catch (const Xapian::Error &error) {
646 notmuch_database_t *notmuch = message->notmuch;
648 if (!notmuch->exception_reported) {
649 _notmuch_database_log(_notmuch_message_database (message), "A Xapian exception occurred creating message: %s\n",
650 error.get_msg().c_str());
651 notmuch->exception_reported = true;
653 return NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
656 return NOTMUCH_PRIVATE_STATUS_SUCCESS;
659 /* Return true if p points at "new" or "cur". */
660 static bool is_maildir (const char *p)
662 return strcmp (p, "cur") == 0 || strcmp (p, "new") == 0;
665 /* Add "folder:" term for directory. */
666 static notmuch_status_t
667 _notmuch_message_add_folder_terms (notmuch_message_t *message,
668 const char *directory)
672 folder = talloc_strdup (NULL, directory);
674 return NOTMUCH_STATUS_OUT_OF_MEMORY;
677 * If the message file is in a leaf directory named "new" or
678 * "cur", presume maildir and index the parent directory. Thus a
679 * "folder:" prefix search matches messages in the specified
680 * maildir folder, i.e. in the specified directory and its "new"
681 * and "cur" subdirectories.
683 * Note that this means the "folder:" prefix can't be used for
684 * distinguishing between message files in "new" or "cur". The
685 * "path:" prefix needs to be used for that.
687 * Note the deliberate difference to _filename_is_in_maildir(). We
688 * don't want to index different things depending on the existence
689 * or non-existence of all maildir sibling directories "new",
690 * "cur", and "tmp". Doing so would be surprising, and difficult
691 * for the user to fix in case all subdirectories were not in
692 * place during indexing.
694 last = strrchr (folder, '/');
696 if (is_maildir (last + 1))
698 } else if (is_maildir (folder)) {
702 _notmuch_message_add_term (message, "folder", folder);
704 talloc_free (folder);
706 message->modified = true;
707 return NOTMUCH_STATUS_SUCCESS;
710 #define RECURSIVE_SUFFIX "/**"
712 /* Add "path:" terms for directory. */
713 static notmuch_status_t
714 _notmuch_message_add_path_terms (notmuch_message_t *message,
715 const char *directory)
717 /* Add exact "path:" term. */
718 _notmuch_message_add_term (message, "path", directory);
720 if (strlen (directory)) {
723 path = talloc_asprintf (NULL, "%s%s", directory, RECURSIVE_SUFFIX);
725 return NOTMUCH_STATUS_OUT_OF_MEMORY;
727 /* Add recursive "path:" terms for directory and all parents. */
728 for (p = path + strlen (path) - 1; p > path; p--) {
730 strcpy (p, RECURSIVE_SUFFIX);
731 _notmuch_message_add_term (message, "path", path);
738 /* Recursive all-matching path:** for consistency. */
739 _notmuch_message_add_term (message, "path", "**");
741 return NOTMUCH_STATUS_SUCCESS;
744 /* Add directory based terms for all filenames of the message. */
745 static notmuch_status_t
746 _notmuch_message_add_directory_terms (void *ctx, notmuch_message_t *message)
748 const char *direntry_prefix = _find_prefix ("file-direntry");
749 int direntry_prefix_len = strlen (direntry_prefix);
750 Xapian::TermIterator i = message->doc.termlist_begin ();
751 notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
753 for (i.skip_to (direntry_prefix); i != message->doc.termlist_end (); i++) {
754 unsigned int directory_id;
755 const char *direntry, *directory;
757 const std::string &term = *i;
759 /* Terminate loop at first term without desired prefix. */
760 if (strncmp (term.c_str (), direntry_prefix, direntry_prefix_len))
763 /* Indicate that there are filenames remaining. */
764 status = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
766 direntry = term.c_str ();
767 direntry += direntry_prefix_len;
769 directory_id = strtol (direntry, &colon, 10);
771 if (colon == NULL || *colon != ':')
772 INTERNAL_ERROR ("malformed direntry");
774 directory = _notmuch_database_get_directory_path (ctx,
778 _notmuch_message_add_folder_terms (message, directory);
779 _notmuch_message_add_path_terms (message, directory);
785 /* Add an additional 'filename' for 'message'.
787 * This change will not be reflected in the database until the next
788 * call to _notmuch_message_sync. */
790 _notmuch_message_add_filename (notmuch_message_t *message,
791 const char *filename)
793 const char *relative, *directory;
794 notmuch_status_t status;
795 void *local = talloc_new (message);
798 if (filename == NULL)
799 INTERNAL_ERROR ("Message filename cannot be NULL.");
801 if (! (message->notmuch->features & NOTMUCH_FEATURE_FILE_TERMS) ||
802 ! (message->notmuch->features & NOTMUCH_FEATURE_BOOL_FOLDER))
803 return NOTMUCH_STATUS_UPGRADE_REQUIRED;
805 relative = _notmuch_database_relative_path (message->notmuch, filename);
807 status = _notmuch_database_split_path (local, relative, &directory, NULL);
811 status = _notmuch_database_filename_to_direntry (
812 local, message->notmuch, filename, NOTMUCH_FIND_CREATE, &direntry);
816 /* New file-direntry allows navigating to this message with
817 * notmuch_directory_get_child_files() . */
818 _notmuch_message_add_term (message, "file-direntry", direntry);
820 _notmuch_message_add_folder_terms (message, directory);
821 _notmuch_message_add_path_terms (message, directory);
825 return NOTMUCH_STATUS_SUCCESS;
828 /* Remove a particular 'filename' from 'message'.
830 * This change will not be reflected in the database until the next
831 * call to _notmuch_message_sync.
833 * If this message still has other filenames, returns
834 * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID.
836 * Note: This function does not remove a document from the database,
837 * even if the specified filename is the only filename for this
838 * message. For that functionality, see
839 * notmuch_database_remove_message. */
841 _notmuch_message_remove_filename (notmuch_message_t *message,
842 const char *filename)
844 void *local = talloc_new (message);
846 notmuch_private_status_t private_status;
847 notmuch_status_t status;
849 if (! (message->notmuch->features & NOTMUCH_FEATURE_FILE_TERMS) ||
850 ! (message->notmuch->features & NOTMUCH_FEATURE_BOOL_FOLDER))
851 return NOTMUCH_STATUS_UPGRADE_REQUIRED;
853 status = _notmuch_database_filename_to_direntry (
854 local, message->notmuch, filename, NOTMUCH_FIND_LOOKUP, &direntry);
855 if (status || !direntry)
858 /* Unlink this file from its parent directory. */
859 private_status = _notmuch_message_remove_term (message,
860 "file-direntry", direntry);
861 status = COERCE_STATUS (private_status,
862 "Unexpected error from _notmuch_message_remove_term");
866 /* Re-synchronize "folder:" and "path:" terms for this message. */
868 /* Remove all "folder:" terms. */
869 _notmuch_message_remove_terms (message, _find_prefix ("folder"));
871 /* Remove all "path:" terms. */
872 _notmuch_message_remove_terms (message, _find_prefix ("path"));
874 /* Add back terms for all remaining filenames of the message. */
875 status = _notmuch_message_add_directory_terms (local, message);
882 /* Upgrade the "folder:" prefix from V1 to V2. */
883 #define FOLDER_PREFIX_V1 "XFOLDER"
884 #define ZFOLDER_PREFIX_V1 "Z" FOLDER_PREFIX_V1
886 _notmuch_message_upgrade_folder (notmuch_message_t *message)
888 /* Remove all old "folder:" terms. */
889 _notmuch_message_remove_terms (message, FOLDER_PREFIX_V1);
891 /* Remove all old "folder:" stemmed terms. */
892 _notmuch_message_remove_terms (message, ZFOLDER_PREFIX_V1);
894 /* Add new boolean "folder:" and "path:" terms. */
895 _notmuch_message_add_directory_terms (message, message);
899 _notmuch_message_talloc_copy_data (notmuch_message_t *message)
901 return talloc_strdup (message, message->doc.get_data ().c_str ());
905 _notmuch_message_clear_data (notmuch_message_t *message)
907 message->doc.set_data ("");
908 message->modified = true;
912 _notmuch_message_ensure_filename_list (notmuch_message_t *message)
914 notmuch_string_node_t *node;
916 if (message->filename_list)
919 _notmuch_message_ensure_metadata (message, message->filename_term_list);
921 message->filename_list = _notmuch_string_list_create (message);
922 node = message->filename_term_list->head;
925 /* A message document created by an old version of notmuch
926 * (prior to rename support) will have the filename in the
927 * data of the document rather than as a file-direntry term.
929 * It would be nice to do the upgrade of the document directly
930 * here, but the database is likely open in read-only mode. */
932 std::string datastr = message->doc.get_data ();
933 const char *data = datastr.c_str ();
936 INTERNAL_ERROR ("message with no filename");
938 _notmuch_string_list_append (message->filename_list, data);
943 for (; node; node = node->next) {
944 void *local = talloc_new (message);
945 const char *db_path, *directory, *basename, *filename;
946 char *colon, *direntry = NULL;
947 unsigned int directory_id;
949 direntry = node->string;
951 directory_id = strtol (direntry, &colon, 10);
953 if (colon == NULL || *colon != ':')
954 INTERNAL_ERROR ("malformed direntry");
956 basename = colon + 1;
960 db_path = notmuch_database_get_path (message->notmuch);
962 directory = _notmuch_database_get_directory_path (local,
966 if (strlen (directory))
967 filename = talloc_asprintf (message, "%s/%s/%s",
968 db_path, directory, basename);
970 filename = talloc_asprintf (message, "%s/%s",
973 _notmuch_string_list_append (message->filename_list, filename);
978 talloc_free (message->filename_term_list);
979 message->filename_term_list = NULL;
983 notmuch_message_get_filename (notmuch_message_t *message)
985 _notmuch_message_ensure_filename_list (message);
987 if (message->filename_list == NULL)
990 if (message->filename_list->head == NULL ||
991 message->filename_list->head->string == NULL)
993 INTERNAL_ERROR ("message with no filename");
996 return message->filename_list->head->string;
999 notmuch_filenames_t *
1000 notmuch_message_get_filenames (notmuch_message_t *message)
1002 _notmuch_message_ensure_filename_list (message);
1004 return _notmuch_filenames_create (message, message->filename_list);
1008 notmuch_message_count_files (notmuch_message_t *message)
1010 _notmuch_message_ensure_filename_list (message);
1012 return _notmuch_string_list_length (message->filename_list);
1016 notmuch_message_get_flag (notmuch_message_t *message,
1017 notmuch_message_flag_t flag)
1019 if (flag == NOTMUCH_MESSAGE_FLAG_GHOST &&
1020 ! NOTMUCH_TEST_BIT (message->lazy_flags, flag))
1021 _notmuch_message_ensure_metadata (message, NULL);
1023 return NOTMUCH_TEST_BIT (message->flags, flag);
1027 notmuch_message_set_flag (notmuch_message_t *message,
1028 notmuch_message_flag_t flag, notmuch_bool_t enable)
1031 NOTMUCH_SET_BIT (&message->flags, flag);
1033 NOTMUCH_CLEAR_BIT (&message->flags, flag);
1034 NOTMUCH_SET_BIT (&message->lazy_flags, flag);
1038 notmuch_message_get_date (notmuch_message_t *message)
1043 value = message->doc.get_value (NOTMUCH_VALUE_TIMESTAMP);
1044 } catch (Xapian::Error &error) {
1045 _notmuch_database_log(_notmuch_message_database (message), "A Xapian exception occurred when reading date: %s\n",
1046 error.get_msg().c_str());
1047 message->notmuch->exception_reported = true;
1052 /* sortable_unserialise is undefined on empty string */
1054 return Xapian::sortable_unserialise (value);
1058 notmuch_message_get_tags (notmuch_message_t *message)
1060 notmuch_tags_t *tags;
1062 _notmuch_message_ensure_metadata (message, message->tag_list);
1064 tags = _notmuch_tags_create (message, message->tag_list);
1065 /* _notmuch_tags_create steals the reference to the tag_list, but
1066 * in this case it's still used by the message, so we add an
1067 * *additional* talloc reference to the list. As a result, it's
1068 * possible to modify the message tags (which talloc_unlink's the
1069 * current list from the message) while still iterating because
1070 * the iterator will keep the current list alive. */
1071 if (!talloc_reference (message, message->tag_list))
1078 _notmuch_message_get_author (notmuch_message_t *message)
1080 return message->author;
1084 _notmuch_message_set_author (notmuch_message_t *message,
1087 if (message->author)
1088 talloc_free(message->author);
1089 message->author = talloc_strdup(message, author);
1094 _notmuch_message_set_header_values (notmuch_message_t *message,
1097 const char *subject)
1101 /* GMime really doesn't want to see a NULL date, so protect its
1103 if (date == NULL || *date == '\0') {
1106 time_value = g_mime_utils_header_decode_date_unix (date);
1108 * Workaround for https://bugzilla.gnome.org/show_bug.cgi?id=779923
1114 message->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
1115 Xapian::sortable_serialise (time_value));
1116 message->doc.add_value (NOTMUCH_VALUE_FROM, from);
1117 message->doc.add_value (NOTMUCH_VALUE_SUBJECT, subject);
1118 message->modified = true;
1121 /* Upgrade a message to support NOTMUCH_FEATURE_LAST_MOD. The caller
1122 * must call _notmuch_message_sync. */
1124 _notmuch_message_upgrade_last_mod (notmuch_message_t *message)
1126 /* _notmuch_message_sync will update the last modification
1127 * revision; we just have to ask it to. */
1128 message->modified = true;
1131 /* Synchronize changes made to message->doc out into the database. */
1133 _notmuch_message_sync (notmuch_message_t *message)
1135 Xapian::WritableDatabase *db;
1137 if (message->notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
1140 if (! message->modified)
1143 /* Update the last modification of this message. */
1144 if (message->notmuch->features & NOTMUCH_FEATURE_LAST_MOD)
1145 /* sortable_serialise gives a reasonably compact encoding,
1146 * which directly translates to reduced IO when scanning the
1147 * value stream. Since it's built for doubles, we only get 53
1148 * effective bits, but that's still enough for the database to
1149 * last a few centuries at 1 million revisions per second. */
1150 message->doc.add_value (NOTMUCH_VALUE_LAST_MOD,
1151 Xapian::sortable_serialise (
1152 _notmuch_database_new_revision (
1153 message->notmuch)));
1155 db = static_cast <Xapian::WritableDatabase *> (message->notmuch->xapian_db);
1156 db->replace_document (message->doc_id, message->doc);
1157 message->modified = false;
1160 /* Delete a message document from the database, leaving a ghost
1161 * message in its place */
1163 _notmuch_message_delete (notmuch_message_t *message)
1165 notmuch_status_t status;
1166 Xapian::WritableDatabase *db;
1167 const char *mid, *tid, *query_string;
1168 notmuch_message_t *ghost;
1169 notmuch_private_status_t private_status;
1170 notmuch_database_t *notmuch;
1171 notmuch_query_t *query;
1172 unsigned int count = 0;
1175 mid = notmuch_message_get_message_id (message);
1176 tid = notmuch_message_get_thread_id (message);
1177 notmuch = message->notmuch;
1179 status = _notmuch_database_ensure_writable (message->notmuch);
1183 db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
1184 db->delete_document (message->doc_id);
1186 /* if this was a ghost to begin with, we are done */
1187 private_status = _notmuch_message_has_term (message, "type", "ghost", &is_ghost);
1189 return COERCE_STATUS (private_status,
1190 "Error trying to determine whether message was a ghost");
1192 return NOTMUCH_STATUS_SUCCESS;
1194 query_string = talloc_asprintf (message, "thread:%s", tid);
1195 query = notmuch_query_create (notmuch, query_string);
1197 return NOTMUCH_STATUS_OUT_OF_MEMORY;
1198 status = notmuch_query_count_messages (query, &count);
1200 notmuch_query_destroy (query);
1205 /* reintroduce a ghost in its place because there are still
1206 * other active messages in this thread: */
1207 ghost = _notmuch_message_create_for_message_id (notmuch, mid, &private_status);
1208 if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
1209 private_status = _notmuch_message_initialize_ghost (ghost, tid);
1210 if (! private_status)
1211 _notmuch_message_sync (ghost);
1212 } else if (private_status == NOTMUCH_PRIVATE_STATUS_SUCCESS) {
1213 /* this is deeply weird, and we should not have gotten
1214 into this state. is there a better error message to
1216 status = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
1219 notmuch_message_destroy (ghost);
1220 status = COERCE_STATUS (private_status, "Error converting to ghost message");
1222 /* the thread is empty; drop all ghost messages from it */
1223 notmuch_messages_t *messages;
1224 status = _notmuch_query_search_documents (query,
1227 if (status == NOTMUCH_STATUS_SUCCESS) {
1228 notmuch_status_t last_error = NOTMUCH_STATUS_SUCCESS;
1229 while (notmuch_messages_valid (messages)) {
1230 message = notmuch_messages_get (messages);
1231 status = _notmuch_message_delete (message);
1232 if (status) /* we'll report the last failure we see;
1233 * if there is more than one failure, we
1234 * forget about previous ones */
1235 last_error = status;
1236 notmuch_message_destroy (message);
1237 notmuch_messages_move_to_next (messages);
1239 status = last_error;
1242 notmuch_query_destroy (query);
1246 /* Transform a blank message into a ghost message. The caller must
1247 * _notmuch_message_sync the message. */
1248 notmuch_private_status_t
1249 _notmuch_message_initialize_ghost (notmuch_message_t *message,
1250 const char *thread_id)
1252 notmuch_private_status_t status;
1254 status = _notmuch_message_add_term (message, "type", "ghost");
1257 status = _notmuch_message_add_term (message, "thread", thread_id);
1261 return NOTMUCH_PRIVATE_STATUS_SUCCESS;
1264 /* Ensure that 'message' is not holding any file object open. Future
1265 * calls to various functions will still automatically open the
1266 * message file as needed.
1269 _notmuch_message_close (notmuch_message_t *message)
1271 if (message->message_file) {
1272 _notmuch_message_file_close (message->message_file);
1273 message->message_file = NULL;
1277 /* Add a name:value term to 'message', (the actual term will be
1278 * encoded by prefixing the value with a short prefix). See
1279 * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
1280 * names to prefix values.
1282 * This change will not be reflected in the database until the next
1283 * call to _notmuch_message_sync. */
1284 notmuch_private_status_t
1285 _notmuch_message_add_term (notmuch_message_t *message,
1286 const char *prefix_name,
1293 return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
1295 term = talloc_asprintf (message, "%s%s",
1296 _find_prefix (prefix_name), value);
1298 if (strlen (term) > NOTMUCH_TERM_MAX)
1299 return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
1301 message->doc.add_term (term, 0);
1302 message->modified = true;
1306 _notmuch_message_invalidate_metadata (message, prefix_name);
1308 return NOTMUCH_PRIVATE_STATUS_SUCCESS;
1311 /* Parse 'text' and add a term to 'message' for each parsed word. Each
1312 * term will be added both prefixed (if prefix_name is not NULL) and
1313 * also non-prefixed). */
1314 notmuch_private_status_t
1315 _notmuch_message_gen_terms (notmuch_message_t *message,
1316 const char *prefix_name,
1319 Xapian::TermGenerator *term_gen = message->notmuch->term_gen;
1322 return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
1324 term_gen->set_document (message->doc);
1327 const char *prefix = _find_prefix (prefix_name);
1329 term_gen->set_termpos (message->termpos);
1330 term_gen->index_text (text, 1, prefix);
1331 /* Create a gap between this an the next terms so they don't
1332 * appear to be a phrase. */
1333 message->termpos = term_gen->get_termpos () + 100;
1335 _notmuch_message_invalidate_metadata (message, prefix_name);
1338 term_gen->set_termpos (message->termpos);
1339 term_gen->index_text (text);
1340 /* Create a term gap, as above. */
1341 message->termpos = term_gen->get_termpos () + 100;
1343 return NOTMUCH_PRIVATE_STATUS_SUCCESS;
1346 /* Remove a name:value term from 'message', (the actual term will be
1347 * encoded by prefixing the value with a short prefix). See
1348 * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
1349 * names to prefix values.
1351 * This change will not be reflected in the database until the next
1352 * call to _notmuch_message_sync. */
1353 notmuch_private_status_t
1354 _notmuch_message_remove_term (notmuch_message_t *message,
1355 const char *prefix_name,
1361 return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
1363 term = talloc_asprintf (message, "%s%s",
1364 _find_prefix (prefix_name), value);
1366 if (strlen (term) > NOTMUCH_TERM_MAX)
1367 return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
1370 message->doc.remove_term (term);
1371 message->modified = true;
1372 } catch (const Xapian::InvalidArgumentError) {
1373 /* We'll let the philosophers try to wrestle with the
1374 * question of whether failing to remove that which was not
1375 * there in the first place is failure. For us, we'll silently
1376 * consider it all good. */
1381 _notmuch_message_invalidate_metadata (message, prefix_name);
1383 return NOTMUCH_PRIVATE_STATUS_SUCCESS;
1386 notmuch_private_status_t
1387 _notmuch_message_has_term (notmuch_message_t *message,
1388 const char *prefix_name,
1394 notmuch_private_status_t status = NOTMUCH_PRIVATE_STATUS_SUCCESS;
1397 return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
1399 term = talloc_asprintf (message, "%s%s",
1400 _find_prefix (prefix_name), value);
1402 if (strlen (term) > NOTMUCH_TERM_MAX)
1403 return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
1406 /* Look for the exact term */
1407 Xapian::TermIterator i = message->doc.termlist_begin ();
1409 if (i != message->doc.termlist_end () &&
1410 !strcmp ((*i).c_str (), term))
1412 } catch (Xapian::Error &error) {
1413 status = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
1422 notmuch_message_add_tag (notmuch_message_t *message, const char *tag)
1424 notmuch_private_status_t private_status;
1425 notmuch_status_t status;
1427 status = _notmuch_database_ensure_writable (message->notmuch);
1432 return NOTMUCH_STATUS_NULL_POINTER;
1434 if (strlen (tag) > NOTMUCH_TAG_MAX)
1435 return NOTMUCH_STATUS_TAG_TOO_LONG;
1437 private_status = _notmuch_message_add_term (message, "tag", tag);
1438 if (private_status) {
1439 INTERNAL_ERROR ("_notmuch_message_add_term return unexpected value: %d\n",
1443 if (! message->frozen)
1444 _notmuch_message_sync (message);
1446 return NOTMUCH_STATUS_SUCCESS;
1450 notmuch_message_remove_tag (notmuch_message_t *message, const char *tag)
1452 notmuch_private_status_t private_status;
1453 notmuch_status_t status;
1455 status = _notmuch_database_ensure_writable (message->notmuch);
1460 return NOTMUCH_STATUS_NULL_POINTER;
1462 if (strlen (tag) > NOTMUCH_TAG_MAX)
1463 return NOTMUCH_STATUS_TAG_TOO_LONG;
1465 private_status = _notmuch_message_remove_term (message, "tag", tag);
1466 if (private_status) {
1467 INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
1471 if (! message->frozen)
1472 _notmuch_message_sync (message);
1474 return NOTMUCH_STATUS_SUCCESS;
1477 /* Is the given filename within a maildir directory?
1479 * Specifically, is the final directory component of 'filename' either
1480 * "cur" or "new". If so, return a pointer to that final directory
1481 * component within 'filename'. If not, return NULL.
1483 * A non-NULL return value is guaranteed to be a valid string pointer
1484 * pointing to the characters "new/" or "cur/", (but not
1488 _filename_is_in_maildir (const char *filename)
1490 const char *slash, *dir = NULL;
1492 /* Find the last '/' separating directory from filename. */
1493 slash = strrchr (filename, '/');
1497 /* Jump back 4 characters to where the previous '/' will be if the
1498 * directory is named "cur" or "new". */
1499 if (slash - filename < 4)
1509 if (STRNCMP_LITERAL (dir, "cur/") == 0 ||
1510 STRNCMP_LITERAL (dir, "new/") == 0)
1519 _ensure_maildir_flags (notmuch_message_t *message, bool force)
1522 notmuch_filenames_t *filenames;
1523 const char *filename, *dir;
1524 char *combined_flags = talloc_strdup (message, "");
1525 int seen_maildir_info = 0;
1527 if (message->maildir_flags) {
1529 talloc_free (message->maildir_flags);
1530 message->maildir_flags = NULL;
1534 for (filenames = notmuch_message_get_filenames (message);
1535 notmuch_filenames_valid (filenames);
1536 notmuch_filenames_move_to_next (filenames))
1538 filename = notmuch_filenames_get (filenames);
1539 dir = _filename_is_in_maildir (filename);
1544 flags = strstr (filename, ":2,");
1546 seen_maildir_info = 1;
1548 combined_flags = talloc_strdup_append (combined_flags, flags);
1549 } else if (STRNCMP_LITERAL (dir, "new/") == 0) {
1550 /* Messages are delivered to new/ with no "info" part, but
1551 * they effectively have default maildir flags. According
1552 * to the spec, we should ignore the info part for
1553 * messages in new/, but some MUAs (mutt) can set maildir
1554 * flags on messages in new/, so we're liberal in what we
1556 seen_maildir_info = 1;
1559 if (seen_maildir_info)
1560 message->maildir_flags = combined_flags;
1564 notmuch_message_has_maildir_flag (notmuch_message_t *message, char flag)
1566 _ensure_maildir_flags (message, false);
1567 return message->maildir_flags && (strchr (message->maildir_flags, flag) != NULL);
1571 notmuch_message_maildir_flags_to_tags (notmuch_message_t *message)
1573 notmuch_status_t status;
1576 _ensure_maildir_flags (message, true);
1577 /* If none of the filenames have any maildir info field (not even
1578 * an empty info with no flags set) then there's no information to
1579 * go on, so do nothing. */
1580 if (! message->maildir_flags)
1581 return NOTMUCH_STATUS_SUCCESS;
1583 status = notmuch_message_freeze (message);
1587 for (i = 0; i < ARRAY_SIZE(flag2tag); i++) {
1588 if ((strchr (message->maildir_flags, flag2tag[i].flag) != NULL)
1590 flag2tag[i].inverse)
1592 status = notmuch_message_add_tag (message, flag2tag[i].tag);
1594 status = notmuch_message_remove_tag (message, flag2tag[i].tag);
1599 status = notmuch_message_thaw (message);
1604 /* From the set of tags on 'message' and the flag2tag table, compute a
1605 * set of maildir-flag actions to be taken, (flags that should be
1606 * either set or cleared).
1608 * The result is returned as two talloced strings: to_set, and to_clear
1611 _get_maildir_flag_actions (notmuch_message_t *message,
1613 char **to_clear_ret)
1615 char *to_set, *to_clear;
1616 notmuch_tags_t *tags;
1620 to_set = talloc_strdup (message, "");
1621 to_clear = talloc_strdup (message, "");
1623 /* First, find flags for all set tags. */
1624 for (tags = notmuch_message_get_tags (message);
1625 notmuch_tags_valid (tags);
1626 notmuch_tags_move_to_next (tags))
1628 tag = notmuch_tags_get (tags);
1630 for (i = 0; i < ARRAY_SIZE (flag2tag); i++) {
1631 if (strcmp (tag, flag2tag[i].tag) == 0) {
1632 if (flag2tag[i].inverse)
1633 to_clear = talloc_asprintf_append (to_clear,
1637 to_set = talloc_asprintf_append (to_set,
1644 /* Then, find the flags for all tags not present. */
1645 for (i = 0; i < ARRAY_SIZE (flag2tag); i++) {
1646 if (flag2tag[i].inverse) {
1647 if (strchr (to_clear, flag2tag[i].flag) == NULL)
1648 to_set = talloc_asprintf_append (to_set, "%c", flag2tag[i].flag);
1650 if (strchr (to_set, flag2tag[i].flag) == NULL)
1651 to_clear = talloc_asprintf_append (to_clear, "%c", flag2tag[i].flag);
1655 *to_set_ret = to_set;
1656 *to_clear_ret = to_clear;
1659 /* Given 'filename' and a set of maildir flags to set and to clear,
1660 * compute the new maildir filename.
1662 * If the existing filename is in the directory "new", the new
1663 * filename will be in the directory "cur", except for the case when
1664 * no flags are changed and the existing filename does not contain
1665 * maildir info (starting with ",2:").
1667 * After a sequence of ":2," in the filename, any subsequent
1668 * single-character flags will be added or removed according to the
1669 * characters in flags_to_set and flags_to_clear. Any existing flags
1670 * not mentioned in either string will remain. The final list of flags
1671 * will be in ASCII order.
1673 * If the original flags seem invalid, (repeated characters or
1674 * non-ASCII ordering of flags), this function will return NULL
1675 * (meaning that renaming would not be safe and should not occur).
1678 _new_maildir_filename (void *ctx,
1679 const char *filename,
1680 const char *flags_to_set,
1681 const char *flags_to_clear)
1683 const char *info, *flags;
1684 unsigned int flag, last_flag;
1685 char *filename_new, *dir;
1687 int flags_in_map = 0;
1688 bool flags_changed = false;
1692 memset (flag_map, 0, sizeof (flag_map));
1694 info = strstr (filename, ":2,");
1697 info = filename + strlen(filename);
1699 /* Loop through existing flags in filename. */
1700 for (flags = info + 3, last_flag = 0;
1702 last_flag = flag, flags++)
1706 /* Original flags not in ASCII order. Abort. */
1707 if (flag < last_flag)
1710 /* Non-ASCII flag. Abort. */
1711 if (flag > sizeof(flag_map) - 1)
1714 /* Repeated flag value. Abort. */
1723 /* Then set and clear our flags from tags. */
1724 for (flags = flags_to_set; *flags; flags++) {
1726 if (flag_map[flag] == 0) {
1729 flags_changed = true;
1733 for (flags = flags_to_clear; *flags; flags++) {
1735 if (flag_map[flag]) {
1738 flags_changed = true;
1742 /* Messages in new/ without maildir info can be kept in new/ if no
1743 * flags have changed. */
1744 dir = (char *) _filename_is_in_maildir (filename);
1745 if (dir && STRNCMP_LITERAL (dir, "new/") == 0 && !*info && !flags_changed)
1746 return talloc_strdup (ctx, filename);
1748 filename_new = (char *) talloc_size (ctx,
1750 strlen (":2,") + flags_in_map + 1);
1751 if (unlikely (filename_new == NULL))
1754 strncpy (filename_new, filename, info - filename);
1755 filename_new[info - filename] = '\0';
1757 strcat (filename_new, ":2,");
1759 s = filename_new + strlen (filename_new);
1760 for (i = 0; i < sizeof (flag_map); i++)
1769 /* If message is in new/ move it under cur/. */
1770 dir = (char *) _filename_is_in_maildir (filename_new);
1771 if (dir && STRNCMP_LITERAL (dir, "new/") == 0)
1772 memcpy (dir, "cur/", 4);
1774 return filename_new;
1778 notmuch_message_tags_to_maildir_flags (notmuch_message_t *message)
1780 notmuch_filenames_t *filenames;
1781 const char *filename;
1783 char *to_set, *to_clear;
1784 notmuch_status_t status = NOTMUCH_STATUS_SUCCESS;
1786 _get_maildir_flag_actions (message, &to_set, &to_clear);
1788 for (filenames = notmuch_message_get_filenames (message);
1789 notmuch_filenames_valid (filenames);
1790 notmuch_filenames_move_to_next (filenames))
1792 filename = notmuch_filenames_get (filenames);
1794 if (! _filename_is_in_maildir (filename))
1797 filename_new = _new_maildir_filename (message, filename,
1799 if (filename_new == NULL)
1802 if (strcmp (filename, filename_new)) {
1804 notmuch_status_t new_status;
1806 err = rename (filename, filename_new);
1810 new_status = _notmuch_message_remove_filename (message,
1812 /* Hold on to only the first error. */
1813 if (! status && new_status
1814 && new_status != NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) {
1815 status = new_status;
1819 new_status = _notmuch_message_add_filename (message,
1821 /* Hold on to only the first error. */
1822 if (! status && new_status) {
1823 status = new_status;
1827 _notmuch_message_sync (message);
1830 talloc_free (filename_new);
1833 talloc_free (to_set);
1834 talloc_free (to_clear);
1840 notmuch_message_remove_all_tags (notmuch_message_t *message)
1842 notmuch_private_status_t private_status;
1843 notmuch_status_t status;
1844 notmuch_tags_t *tags;
1847 status = _notmuch_database_ensure_writable (message->notmuch);
1851 for (tags = notmuch_message_get_tags (message);
1852 notmuch_tags_valid (tags);
1853 notmuch_tags_move_to_next (tags))
1855 tag = notmuch_tags_get (tags);
1857 private_status = _notmuch_message_remove_term (message, "tag", tag);
1858 if (private_status) {
1859 INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
1864 if (! message->frozen)
1865 _notmuch_message_sync (message);
1868 return NOTMUCH_STATUS_SUCCESS;
1872 notmuch_message_freeze (notmuch_message_t *message)
1874 notmuch_status_t status;
1876 status = _notmuch_database_ensure_writable (message->notmuch);
1882 return NOTMUCH_STATUS_SUCCESS;
1886 notmuch_message_thaw (notmuch_message_t *message)
1888 notmuch_status_t status;
1890 status = _notmuch_database_ensure_writable (message->notmuch);
1894 if (message->frozen > 0) {
1896 if (message->frozen == 0)
1897 _notmuch_message_sync (message);
1898 return NOTMUCH_STATUS_SUCCESS;
1900 return NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW;
1905 notmuch_message_destroy (notmuch_message_t *message)
1907 talloc_free (message);
1910 notmuch_database_t *
1911 _notmuch_message_database (notmuch_message_t *message)
1913 return message->notmuch;
1917 _notmuch_message_ensure_property_map (notmuch_message_t *message)
1919 notmuch_string_node_t *node;
1921 if (message->property_map)
1924 _notmuch_message_ensure_metadata (message, message->property_term_list);
1926 message->property_map = _notmuch_string_map_create (message);
1928 for (node = message->property_term_list->head; node; node = node->next) {
1932 value = strchr(node->string, '=');
1934 INTERNAL_ERROR ("malformed property term");
1940 _notmuch_string_map_append (message->property_map, key, value);
1944 talloc_free (message->property_term_list);
1945 message->property_term_list = NULL;
1948 notmuch_string_map_t *
1949 _notmuch_message_property_map (notmuch_message_t *message)
1951 _notmuch_message_ensure_property_map (message);
1953 return message->property_map;
1957 _notmuch_message_frozen (notmuch_message_t *message)
1959 return message->frozen;
1963 notmuch_message_reindex (notmuch_message_t *message,
1964 notmuch_indexopts_t unused (*indexopts))
1966 notmuch_database_t *notmuch = NULL;
1967 notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
1968 notmuch_private_status_t private_status;
1969 notmuch_filenames_t *orig_filenames = NULL;
1970 const char *orig_thread_id = NULL;
1971 notmuch_message_file_t *message_file = NULL;
1975 if (message == NULL)
1976 return NOTMUCH_STATUS_NULL_POINTER;
1978 /* Save in case we need to delete message */
1979 orig_thread_id = notmuch_message_get_thread_id (message);
1980 if (!orig_thread_id) {
1981 /* XXX TODO: make up new error return? */
1982 INTERNAL_ERROR ("message without thread-id");
1985 /* strdup it because the metadata may be invalidated */
1986 orig_thread_id = talloc_strdup (message, orig_thread_id);
1988 notmuch = _notmuch_message_database (message);
1990 ret = _notmuch_database_ensure_writable (notmuch);
1994 orig_filenames = notmuch_message_get_filenames (message);
1996 private_status = _notmuch_message_remove_indexed_terms (message);
1997 if (private_status) {
1998 ret = COERCE_STATUS(private_status, "error removing terms");
2002 /* re-add the filenames with the associated indexopts */
2003 for (; notmuch_filenames_valid (orig_filenames);
2004 notmuch_filenames_move_to_next (orig_filenames)) {
2007 const char *from, *to, *subject;
2008 char *message_id = NULL;
2009 const char *thread_id = NULL;
2011 const char *filename = notmuch_filenames_get (orig_filenames);
2013 message_file = _notmuch_message_file_open (notmuch, filename);
2014 if (message_file == NULL)
2017 ret = _notmuch_message_file_get_headers (message_file,
2018 &from, &subject, &to, &date,
2023 /* XXX TODO: deal with changing message id? */
2025 _notmuch_message_add_filename (message, filename);
2027 ret = _notmuch_database_link_message_to_parents (notmuch, message,
2033 if (thread_id == NULL)
2034 thread_id = orig_thread_id;
2036 _notmuch_message_add_term (message, "thread", thread_id);
2037 /* Take header values only from first filename */
2039 _notmuch_message_set_header_values (message, date, from, subject);
2041 ret = _notmuch_message_index_file (message, message_file);
2043 if (ret == NOTMUCH_STATUS_FILE_ERROR)
2049 _notmuch_message_file_close (message_file);
2050 message_file = NULL;
2053 /* put back thread id to help cleanup */
2054 _notmuch_message_add_term (message, "thread", orig_thread_id);
2055 ret = _notmuch_message_delete (message);
2057 _notmuch_message_sync (message);
2062 _notmuch_message_file_close (message_file);
2064 /* XXX TODO destroy orig_filenames? */