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 http://www.gnu.org/licenses/ .
18 * Author: Carl Worth <cworth@cworth.org>
21 #include "notmuch-private.h"
22 #include "database-private.h"
26 struct _notmuch_message {
27 notmuch_database_t *notmuch;
35 typedef struct _notmuch_terms {
37 Xapian::TermIterator iterator;
38 Xapian::TermIterator iterator_end;
41 struct _notmuch_tags {
42 notmuch_terms_t terms;
45 /* "128 bits of thread-id ought to be enough for anybody" */
46 #define NOTMUCH_THREAD_ID_BITS 128
47 #define NOTMUCH_THREAD_ID_DIGITS (NOTMUCH_THREAD_ID_BITS / 4)
48 typedef struct _thread_id {
49 char str[NOTMUCH_THREAD_ID_DIGITS + 1];
52 /* We end up having to call the destructor explicitly because we had
53 * to use "placement new" in order to initialize C++ objects within a
54 * block that we allocated with talloc. So C++ is making talloc
55 * slightly less simple to use, (we wouldn't need
56 * talloc_set_destructor at all otherwise).
59 _notmuch_message_destructor (notmuch_message_t *message)
61 message->doc.~Document ();
66 /* Create a new notmuch_message_t object for an existing document in
69 * Here, 'talloc owner' is an optional talloc context to which the new
70 * message will belong. This allows for the caller to not bother
71 * calling notmuch_message_destroy on the message, and no that all
72 * memory will be reclaimed with 'talloc_owner' is free. The caller
73 * still can call notmuch_message_destroy when finished with the
76 * The 'talloc_owner' argument can also be NULL, in which case the
77 * caller *is* responsible for calling notmuch_message_destroy.
79 * If no document exists in the database with document ID of 'doc_id'
80 * then this function returns NULL and optionally sets *status to
81 * NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND.
83 * This function can also fail to due lack of available memory,
84 * returning NULL and optionally setting *status to
85 * NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY.
87 * The caller can pass NULL for status if uninterested in
88 * distinguishing these two cases.
91 _notmuch_message_create (const void *talloc_owner,
92 notmuch_database_t *notmuch,
94 notmuch_private_status_t *status)
96 notmuch_message_t *message;
99 *status = NOTMUCH_PRIVATE_STATUS_SUCCESS;
101 message = talloc (talloc_owner, notmuch_message_t);
102 if (unlikely (message == NULL)) {
104 *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
108 message->notmuch = notmuch;
109 message->doc_id = doc_id;
110 message->message_id = NULL; /* lazily created */
111 message->thread_id = NULL; /* lazily created */
112 message->filename = NULL; /* lazily created */
114 /* This is C++'s creepy "placement new", which is really just an
115 * ugly way to call a constructor for a pre-allocated object. So
116 * it's really not an error to not be checking for OUT_OF_MEMORY
117 * here, since this "new" isn't actually allocating memory. This
118 * is language-design comedy of the wrong kind. */
120 new (&message->doc) Xapian::Document;
122 talloc_set_destructor (message, _notmuch_message_destructor);
125 message->doc = notmuch->xapian_db->get_document (doc_id);
126 } catch (const Xapian::DocNotFoundError &error) {
127 talloc_free (message);
129 *status = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
136 /* Create a new notmuch_message_t object for a specific message ID,
137 * (which may or may not already exist in the databas).
139 * Here, 'talloc owner' is an optional talloc context to which the new
140 * message will belong. This allows for the caller to not bother
141 * calling notmuch_message_destroy on the message, and no that all
142 * memory will be reclaimed with 'talloc_owner' is free. The caller
143 * still can call notmuch_message_destroy when finished with the
144 * message if desired.
146 * The 'talloc_owner' argument can also be NULL, in which case the
147 * caller *is* responsible for calling notmuch_message_destroy.
149 * If there is already a document with message ID 'message_id' in the
150 * database, then the returned message can be used to query/modify the
151 * document. Otherwise, a new document will be inserted into the
152 * database before this function returns.
154 * If an error occurs, this function will return NULL and *status
155 * will be set as appropriate. (The status pointer argument must
159 _notmuch_message_create_for_message_id (const void *talloc_owner,
160 notmuch_database_t *notmuch,
161 const char *message_id,
162 notmuch_status_t *status)
164 notmuch_private_status_t private_status;
165 notmuch_message_t *message;
166 Xapian::Document doc;
170 *status = NOTMUCH_STATUS_SUCCESS;
172 message = notmuch_database_find_message (notmuch, message_id);
174 return talloc_steal (talloc_owner, message);
176 term = talloc_asprintf (NULL, "%s%s",
177 _find_prefix ("id"), message_id);
179 *status = NOTMUCH_STATUS_OUT_OF_MEMORY;
187 doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id);
189 doc_id = notmuch->xapian_db->add_document (doc);
190 } catch (const Xapian::Error &error) {
191 *status = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
195 message = _notmuch_message_create (talloc_owner, notmuch,
196 doc_id, &private_status);
198 *status = COERCE_STATUS (private_status,
199 "Failed to find dcocument after inserting it.");
205 notmuch_message_get_message_id (notmuch_message_t *message)
207 Xapian::TermIterator i;
209 if (message->message_id)
210 return message->message_id;
212 i = message->doc.termlist_begin ();
213 i.skip_to (_find_prefix ("id"));
215 if (i == message->doc.termlist_end ())
216 INTERNAL_ERROR ("Message with document ID of %d has no message ID.\n",
219 message->message_id = talloc_strdup (message, (*i).c_str () + 1);
221 #if DEBUG_DATABASE_SANITY
224 if (i != message->doc.termlist_end () &&
225 strncmp ((*i).c_str (), _find_prefix ("id"),
226 strlen (_find_prefix ("id"))) == 0)
228 INTERNAL_ERROR ("Mail (doc_id: %d) has duplicate message IDs",
233 return message->message_id;
237 notmuch_message_get_thread_id (notmuch_message_t *message)
239 Xapian::TermIterator i;
241 if (message->thread_id)
242 return message->thread_id;
244 i = message->doc.termlist_begin ();
245 i.skip_to (_find_prefix ("thread"));
247 if (i == message->doc.termlist_end ())
248 INTERNAL_ERROR ("Message with document ID of %d has no thread ID.\n",
251 message->thread_id = talloc_strdup (message, (*i).c_str () + 1);
253 #if DEBUG_DATABASE_SANITY
256 if (i != message->doc.termlist_end () &&
257 strncmp ((*i).c_str (), _find_prefix ("thread"),
258 strlen (_find_prefix ("thread"))) == 0)
260 INTERNAL_ERROR ("Message with document ID of %d has duplicate thread IDs.\n",
265 return message->thread_id;
268 /* Set the filename for 'message' to 'filename'.
270 * XXX: We should still figure out if we think it's important to store
271 * multiple filenames for email messages with identical message IDs.
273 * This change will not be reflected in the database until the next
274 * call to _notmuch_message_set_sync. */
276 _notmuch_message_set_filename (notmuch_message_t *message,
277 const char *filename)
279 if (message->filename)
280 talloc_free (message->filename);
281 message->doc.set_data (filename);
285 notmuch_message_get_filename (notmuch_message_t *message)
287 std::string filename_str;
289 if (message->filename)
290 return message->filename;
292 filename_str = message->doc.get_data ();
293 message->filename = talloc_strdup (message, filename_str.c_str ());
295 return message->filename;
298 /* We end up having to call the destructors explicitly because we had
299 * to use "placement new" in order to initialize C++ objects within a
300 * block that we allocated with talloc. So C++ is making talloc
301 * slightly less simple to use, (we wouldn't need
302 * talloc_set_destructor at all otherwise).
305 _notmuch_terms_destructor (notmuch_terms_t *terms)
307 terms->iterator.~TermIterator ();
308 terms->iterator_end.~TermIterator ();
313 static notmuch_terms_t *
314 _notmuch_terms_create (void *ctx,
315 Xapian::Document doc,
316 const char *prefix_name)
318 notmuch_terms_t *terms;
319 const char *prefix = _find_prefix (prefix_name);
321 /* Currently, notmuch_terms_t is written with the assumption that
322 * any prefix its derivatives use will be only a single
324 assert (strlen (prefix) == 1);
326 terms = talloc (ctx, notmuch_terms_t);
327 if (unlikely (terms == NULL))
330 terms->prefix_char = *prefix;
331 new (&terms->iterator) Xapian::TermIterator;
332 new (&terms->iterator_end) Xapian::TermIterator;
334 talloc_set_destructor (terms, _notmuch_terms_destructor);
336 terms->iterator = doc.termlist_begin ();
337 terms->iterator.skip_to (prefix);
338 terms->iterator_end = doc.termlist_end ();
343 /* The assertion is to ensure that 'type' is a derivative of
344 * notmuch_terms_t in that it contains a notmuch_terms_t as its first
345 * member. We do this by name of 'terms' as opposed to type, because
346 * that's as clever as I've been so far. */
347 #define _notmuch_terms_create_type(ctx, doc, prefix_name, type) \
348 (COMPILE_TIME_ASSERT(offsetof(type, terms) == 0), \
349 (type *) _notmuch_terms_create (ctx, doc, prefix_name))
352 notmuch_message_get_tags (notmuch_message_t *message)
354 return _notmuch_terms_create_type (message, message->doc, "tag",
359 _notmuch_message_set_date (notmuch_message_t *message,
364 time_value = notmuch_parse_date (date, NULL);
366 message->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
367 Xapian::sortable_serialise (time_value));
371 thread_id_generate (thread_id_t *thread_id)
373 static int seeded = 0;
380 dev_random = fopen ("/dev/random", "r");
381 if (dev_random == NULL) {
384 fread ((void *) &value, sizeof (value), 1, dev_random);
392 for (i = 0; i < NOTMUCH_THREAD_ID_DIGITS; i += 8) {
394 sprintf (s, "%08x", value);
400 _notmuch_message_ensure_thread_id (notmuch_message_t *message)
402 /* If not part of any existing thread, generate a new thread_id. */
403 thread_id_t thread_id;
405 thread_id_generate (&thread_id);
406 _notmuch_message_add_term (message, "thread", thread_id.str);
409 /* Synchronize changes made to message->doc out into the database. */
411 _notmuch_message_sync (notmuch_message_t *message)
413 Xapian::WritableDatabase *db = message->notmuch->xapian_db;
415 db->replace_document (message->doc_id, message->doc);
418 /* Add a name:value term to 'message', (the actual term will be
419 * encoded by prefixing the value with a short prefix). See
420 * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
421 * names to prefix values.
423 * This change will not be reflected in the database until the next
424 * call to _notmuch_message_set_sync. */
425 notmuch_private_status_t
426 _notmuch_message_add_term (notmuch_message_t *message,
427 const char *prefix_name,
434 return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
436 term = talloc_asprintf (message, "%s%s",
437 _find_prefix (prefix_name), value);
439 if (strlen (term) > NOTMUCH_TERM_MAX)
440 return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
442 message->doc.add_term (term);
446 return NOTMUCH_PRIVATE_STATUS_SUCCESS;
449 /* Remove a name:value term from 'message', (the actual term will be
450 * encoded by prefixing the value with a short prefix). See
451 * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
452 * names to prefix values.
454 * This change will not be reflected in the database until the next
455 * call to _notmuch_message_set_sync. */
456 notmuch_private_status_t
457 _notmuch_message_remove_term (notmuch_message_t *message,
458 const char *prefix_name,
464 return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
466 term = talloc_asprintf (message, "%s%s",
467 _find_prefix (prefix_name), value);
469 if (strlen (term) > NOTMUCH_TERM_MAX)
470 return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
472 message->doc.remove_term (term);
476 return NOTMUCH_PRIVATE_STATUS_SUCCESS;
480 notmuch_message_add_tag (notmuch_message_t *message, const char *tag)
482 notmuch_private_status_t status;
485 return NOTMUCH_STATUS_NULL_POINTER;
487 if (strlen (tag) > NOTMUCH_TAG_MAX)
488 return NOTMUCH_STATUS_TAG_TOO_LONG;
490 status = _notmuch_message_add_term (message, "tag", tag);
492 INTERNAL_ERROR ("_notmuch_message_add_term return unexpected value: %d\n",
496 _notmuch_message_sync (message);
498 return NOTMUCH_STATUS_SUCCESS;
502 notmuch_message_remove_tag (notmuch_message_t *message, const char *tag)
504 notmuch_private_status_t status;
507 return NOTMUCH_STATUS_NULL_POINTER;
509 if (strlen (tag) > NOTMUCH_TAG_MAX)
510 return NOTMUCH_STATUS_TAG_TOO_LONG;
512 status = _notmuch_message_remove_term (message, "tag", tag);
514 INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
518 _notmuch_message_sync (message);
520 return NOTMUCH_STATUS_SUCCESS;
524 notmuch_message_destroy (notmuch_message_t *message)
526 talloc_free (message);
529 static notmuch_bool_t
530 _notmuch_terms_has_more (notmuch_terms_t *terms)
534 if (terms->iterator == terms->iterator_end)
537 s = *terms->iterator;
538 if (! s.empty () && s[0] == terms->prefix_char)
545 _notmuch_terms_get (notmuch_terms_t *terms)
547 return talloc_strdup (terms, (*terms->iterator).c_str () + 1);
551 _notmuch_terms_advance (notmuch_terms_t *terms)
557 _notmuch_terms_destroy (notmuch_terms_t *terms)
563 notmuch_tags_has_more (notmuch_tags_t *tags)
565 return _notmuch_terms_has_more (&tags->terms);
569 notmuch_tags_get (notmuch_tags_t *tags)
571 return _notmuch_terms_get (&tags->terms);
575 notmuch_tags_advance (notmuch_tags_t *tags)
577 return _notmuch_terms_advance (&tags->terms);
581 notmuch_tags_destroy (notmuch_tags_t *tags)
583 return _notmuch_terms_destroy (&tags->terms);