]> git.notmuchmail.org Git - notmuch/blob - lib/message.cc
Include <stdint.h> to get uint32_t in C++ file with gcc 4.4
[notmuch] / lib / message.cc
1 /* message.cc - Results of message-based searches from a notmuch database
2  *
3  * Copyright © 2009 Carl Worth
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see http://www.gnu.org/licenses/ .
17  *
18  * Author: Carl Worth <cworth@cworth.org>
19  */
20
21 #include "notmuch-private.h"
22 #include "database-private.h"
23
24 #include <stdint.h>
25
26 #include <gmime/gmime.h>
27
28 #include <xapian.h>
29
30 struct _notmuch_message {
31     notmuch_database_t *notmuch;
32     Xapian::docid doc_id;
33     int frozen;
34     char *message_id;
35     char *thread_id;
36     char *filename;
37     notmuch_message_file_t *message_file;
38     notmuch_message_list_t *replies;
39
40     Xapian::Document doc;
41 };
42
43 /* "128 bits of thread-id ought to be enough for anybody" */
44 #define NOTMUCH_THREAD_ID_BITS   128
45 #define NOTMUCH_THREAD_ID_DIGITS (NOTMUCH_THREAD_ID_BITS / 4)
46 typedef struct _thread_id {
47     char str[NOTMUCH_THREAD_ID_DIGITS + 1];
48 } thread_id_t;
49
50 /* We end up having to call the destructor explicitly because we had
51  * to use "placement new" in order to initialize C++ objects within a
52  * block that we allocated with talloc. So C++ is making talloc
53  * slightly less simple to use, (we wouldn't need
54  * talloc_set_destructor at all otherwise).
55  */
56 static int
57 _notmuch_message_destructor (notmuch_message_t *message)
58 {
59     message->doc.~Document ();
60
61     return 0;
62 }
63
64 /* Create a new notmuch_message_t object for an existing document in
65  * the database.
66  *
67  * Here, 'talloc owner' is an optional talloc context to which the new
68  * message will belong. This allows for the caller to not bother
69  * calling notmuch_message_destroy on the message, and no that all
70  * memory will be reclaimed with 'talloc_owner' is free. The caller
71  * still can call notmuch_message_destroy when finished with the
72  * message if desired.
73  *
74  * The 'talloc_owner' argument can also be NULL, in which case the
75  * caller *is* responsible for calling notmuch_message_destroy.
76  *
77  * If no document exists in the database with document ID of 'doc_id'
78  * then this function returns NULL and optionally sets *status to
79  * NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND.
80  *
81  * This function can also fail to due lack of available memory,
82  * returning NULL and optionally setting *status to
83  * NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY.
84  *
85  * The caller can pass NULL for status if uninterested in
86  * distinguishing these two cases.
87  */
88 notmuch_message_t *
89 _notmuch_message_create (const void *talloc_owner,
90                          notmuch_database_t *notmuch,
91                          unsigned int doc_id,
92                          notmuch_private_status_t *status)
93 {
94     notmuch_message_t *message;
95
96     if (status)
97         *status = NOTMUCH_PRIVATE_STATUS_SUCCESS;
98
99     message = talloc (talloc_owner, notmuch_message_t);
100     if (unlikely (message == NULL)) {
101         if (status)
102             *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
103         return NULL;
104     }
105
106     message->notmuch = notmuch;
107     message->doc_id = doc_id;
108
109     message->frozen = 0;
110
111     /* Each of these will be lazily created as needed. */
112     message->message_id = NULL;
113     message->thread_id = NULL;
114     message->filename = NULL;
115     message->message_file = NULL;
116
117     message->replies = _notmuch_message_list_create (message);
118     if (unlikely (message->replies == NULL)) {
119         if (status)
120             *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
121         return NULL;
122     }
123
124     /* This is C++'s creepy "placement new", which is really just an
125      * ugly way to call a constructor for a pre-allocated object. So
126      * it's really not an error to not be checking for OUT_OF_MEMORY
127      * here, since this "new" isn't actually allocating memory. This
128      * is language-design comedy of the wrong kind. */
129
130     new (&message->doc) Xapian::Document;
131
132     talloc_set_destructor (message, _notmuch_message_destructor);
133
134     try {
135         message->doc = notmuch->xapian_db->get_document (doc_id);
136     } catch (const Xapian::DocNotFoundError &error) {
137         talloc_free (message);
138         if (status)
139             *status = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
140         return NULL;
141     }
142
143     return message;
144 }
145
146 /* Create a new notmuch_message_t object for a specific message ID,
147  * (which may or may not already exist in the databas).
148  *
149  * Here, 'talloc owner' is an optional talloc context to which the new
150  * message will belong. This allows for the caller to not bother
151  * calling notmuch_message_destroy on the message, and no that all
152  * memory will be reclaimed with 'talloc_owner' is free. The caller
153  * still can call notmuch_message_destroy when finished with the
154  * message if desired.
155  *
156  * The 'talloc_owner' argument can also be NULL, in which case the
157  * caller *is* responsible for calling notmuch_message_destroy.
158  *
159  * If there is already a document with message ID 'message_id' in the
160  * database, then the returned message can be used to query/modify the
161  * document. Otherwise, a new document will be inserted into the
162  * database before this function returns, (and *status will be set
163  * to NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND).
164  *
165  * If an error occurs, this function will return NULL and *status
166  * will be set as appropriate. (The status pointer argument must
167  * not be NULL.)
168  */
169 notmuch_message_t *
170 _notmuch_message_create_for_message_id (const void *talloc_owner,
171                                         notmuch_database_t *notmuch,
172                                         const char *message_id,
173                                         notmuch_private_status_t *status_ret)
174 {
175     notmuch_message_t *message;
176     Xapian::Document doc;
177     unsigned int doc_id;
178     char *term;
179
180     *status_ret = NOTMUCH_PRIVATE_STATUS_SUCCESS;
181
182     message = notmuch_database_find_message (notmuch, message_id);
183     if (message)
184         return talloc_steal (talloc_owner, message);
185
186     term = talloc_asprintf (NULL, "%s%s",
187                             _find_prefix ("id"), message_id);
188     if (term == NULL) {
189         *status_ret = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
190         return NULL;
191     }
192
193     try {
194         doc.add_term (term);
195         talloc_free (term);
196
197         doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id);
198
199         doc_id = notmuch->xapian_db->add_document (doc);
200     } catch (const Xapian::Error &error) {
201         *status_ret = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
202         return NULL;
203     }
204
205     message = _notmuch_message_create (talloc_owner, notmuch,
206                                        doc_id, status_ret);
207
208     /* We want to inform the caller that we had to create a new
209      * document. */
210     if (*status_ret == NOTMUCH_PRIVATE_STATUS_SUCCESS)
211         *status_ret = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
212
213     return message;
214 }
215
216 const char *
217 notmuch_message_get_message_id (notmuch_message_t *message)
218 {
219     Xapian::TermIterator i;
220
221     if (message->message_id)
222         return message->message_id;
223
224     i = message->doc.termlist_begin ();
225     i.skip_to (_find_prefix ("id"));
226
227     if (i == message->doc.termlist_end ())
228         INTERNAL_ERROR ("Message with document ID of %d has no message ID.\n",
229                         message->doc_id);
230
231     message->message_id = talloc_strdup (message, (*i).c_str () + 1);
232
233 #if DEBUG_DATABASE_SANITY
234     i++;
235
236     if (i != message->doc.termlist_end () &&
237         strncmp ((*i).c_str (), _find_prefix ("id"),
238                  strlen (_find_prefix ("id"))) == 0)
239     {
240         INTERNAL_ERROR ("Mail (doc_id: %d) has duplicate message IDs",
241                         message->doc_id);
242     }
243 #endif
244
245     return message->message_id;
246 }
247
248 static void
249 _notmuch_message_ensure_message_file (notmuch_message_t *message)
250 {
251     const char *filename;
252
253     if (message->message_file)
254         return;
255
256     filename = notmuch_message_get_filename (message);
257     if (unlikely (filename == NULL))
258         return;
259
260     message->message_file = _notmuch_message_file_open_ctx (message, filename);
261 }
262
263 const char *
264 notmuch_message_get_header (notmuch_message_t *message, const char *header)
265 {
266     _notmuch_message_ensure_message_file (message);
267     if (message->message_file == NULL)
268         return NULL;
269
270     return notmuch_message_file_get_header (message->message_file, header);
271 }
272
273 /* XXX: We probably want to store the In-Reply-To header in the
274  * database (separate from the References message IDs) so that we can
275  * fetch it out again without having to go load the message file. */
276 const char *
277 _notmuch_message_get_in_reply_to (notmuch_message_t *message)
278 {
279     return _parse_message_id (message,
280                               notmuch_message_get_header (message,
281                                                           "in-reply-to"),
282                               NULL);
283 }
284
285 const char *
286 notmuch_message_get_thread_id (notmuch_message_t *message)
287 {
288     Xapian::TermIterator i;
289
290     if (message->thread_id)
291         return message->thread_id;
292
293     i = message->doc.termlist_begin ();
294     i.skip_to (_find_prefix ("thread"));
295
296     if (i == message->doc.termlist_end ())
297         INTERNAL_ERROR ("Message with document ID of %d has no thread ID.\n",
298                         message->doc_id);
299
300     message->thread_id = talloc_strdup (message, (*i).c_str () + 1);
301
302 #if DEBUG_DATABASE_SANITY
303     i++;
304
305     if (i != message->doc.termlist_end () &&
306         strncmp ((*i).c_str (), _find_prefix ("thread"),
307                  strlen (_find_prefix ("thread"))) == 0)
308     {
309         INTERNAL_ERROR ("Message %s has duplicate thread IDs: %s and %s\n",
310                         notmuch_message_get_message_id (message),
311                         message->thread_id,
312                         (*i).c_str () + 1);
313     }
314 #endif
315
316     return message->thread_id;
317 }
318
319 void
320 _notmuch_message_add_reply (notmuch_message_t *message,
321                             notmuch_message_node_t *reply)
322 {
323     _notmuch_message_list_append (message->replies, reply);
324 }
325
326 notmuch_messages_t *
327 notmuch_message_get_replies (notmuch_message_t *message)
328 {
329     return _notmuch_messages_create (message->replies);
330 }
331
332 /* Set the filename for 'message' to 'filename'.
333  *
334  * XXX: We should still figure out if we think it's important to store
335  * multiple filenames for email messages with identical message IDs.
336  *
337  * This change will not be reflected in the database until the next
338  * call to _notmuch_message_set_sync. */
339 void
340 _notmuch_message_set_filename (notmuch_message_t *message,
341                                const char *filename)
342 {
343     const char *s;
344     const char *db_path;
345     unsigned int db_path_len;
346
347     if (message->filename) {
348         talloc_free (message->filename);
349         message->filename = NULL;
350     }
351
352     if (filename == NULL)
353         INTERNAL_ERROR ("Message filename cannot be NULL.");
354
355     s = filename;
356
357     db_path = notmuch_database_get_path (message->notmuch);
358     db_path_len = strlen (db_path);
359
360     if (*s == '/' && strncmp (s, db_path, db_path_len) == 0
361         && strlen (s) > db_path_len)
362     {
363         s += db_path_len + 1;
364     }
365
366     message->doc.set_data (s);
367 }
368
369 const char *
370 notmuch_message_get_filename (notmuch_message_t *message)
371 {
372     std::string filename_str;
373     const char *db_path;
374
375     if (message->filename)
376         return message->filename;
377
378     filename_str = message->doc.get_data ();
379     db_path = notmuch_database_get_path (message->notmuch);
380
381     if (filename_str[0] != '/')
382         message->filename = talloc_asprintf (message, "%s/%s", db_path,
383                                              filename_str.c_str ());
384     else
385         message->filename = talloc_strdup (message, filename_str.c_str ());
386
387     return message->filename;
388 }
389
390 time_t
391 notmuch_message_get_date (notmuch_message_t *message)
392 {
393     std::string value;
394
395     try {
396         value = message->doc.get_value (NOTMUCH_VALUE_TIMESTAMP);
397     } catch (Xapian::Error &error) {
398         INTERNAL_ERROR ("Failed to read timestamp value from document.");
399         return 0;
400     }
401
402     return Xapian::sortable_unserialise (value);
403 }
404
405 notmuch_tags_t *
406 notmuch_message_get_tags (notmuch_message_t *message)
407 {
408     const char *prefix = _find_prefix ("tag");
409     Xapian::TermIterator i, end;
410     notmuch_tags_t *tags;
411     std::string tag;
412
413     /* Currently this iteration is written with the assumption that
414      * "tag" has a single-character prefix. */
415     assert (strlen (prefix) == 1);
416
417     tags = _notmuch_tags_create (message);
418     if (unlikely (tags == NULL))
419         return NULL;
420
421     i = message->doc.termlist_begin ();
422     end = message->doc.termlist_end ();
423
424     i.skip_to (prefix);
425
426     while (1) {
427         tag = *i;
428
429         if (tag.empty () || tag[0] != *prefix)
430             break;
431
432         _notmuch_tags_add_tag (tags, tag.c_str () + 1);
433
434         i++;
435     }
436
437     _notmuch_tags_prepare_iterator (tags);
438
439     return tags;
440 }
441
442 void
443 _notmuch_message_set_date (notmuch_message_t *message,
444                            const char *date)
445 {
446     time_t time_value;
447
448     /* GMime really doesn't want to see a NULL date, so protect its
449      * sensibilities. */
450     if (date == NULL)
451         time_value = 0;
452     else
453         time_value = g_mime_utils_header_decode_date (date, NULL);
454
455     message->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
456                             Xapian::sortable_serialise (time_value));
457 }
458
459 static void
460 thread_id_generate (thread_id_t *thread_id)
461 {
462     static int seeded = 0;
463     FILE *dev_random;
464     uint32_t value;
465     char *s;
466     int i;
467
468     if (! seeded) {
469         dev_random = fopen ("/dev/random", "r");
470         if (dev_random == NULL) {
471             srand (time (NULL));
472         } else {
473             fread ((void *) &value, sizeof (value), 1, dev_random);
474             srand (value);
475             fclose (dev_random);
476         }
477         seeded = 1;
478     }
479
480     s = thread_id->str;
481     for (i = 0; i < NOTMUCH_THREAD_ID_DIGITS; i += 8) {
482         value = rand ();
483         sprintf (s, "%08x", value);
484         s += 8;
485     }
486 }
487
488 void
489 _notmuch_message_ensure_thread_id (notmuch_message_t *message)
490 {
491     /* If not part of any existing thread, generate a new thread_id. */
492     thread_id_t thread_id;
493
494     thread_id_generate (&thread_id);
495     _notmuch_message_add_term (message, "thread", thread_id.str);
496 }
497
498 /* Synchronize changes made to message->doc out into the database. */
499 void
500 _notmuch_message_sync (notmuch_message_t *message)
501 {
502     Xapian::WritableDatabase *db = message->notmuch->xapian_db;
503
504     db->replace_document (message->doc_id, message->doc);
505 }
506
507 /* Add a name:value term to 'message', (the actual term will be
508  * encoded by prefixing the value with a short prefix). See
509  * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
510  * names to prefix values.
511  *
512  * This change will not be reflected in the database until the next
513  * call to _notmuch_message_set_sync. */
514 notmuch_private_status_t
515 _notmuch_message_add_term (notmuch_message_t *message,
516                            const char *prefix_name,
517                            const char *value)
518 {
519
520     char *term;
521
522     if (value == NULL)
523         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
524
525     term = talloc_asprintf (message, "%s%s",
526                             _find_prefix (prefix_name), value);
527
528     if (strlen (term) > NOTMUCH_TERM_MAX)
529         return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
530
531     message->doc.add_term (term);
532
533     talloc_free (term);
534
535     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
536 }
537
538 /* Parse 'text' and add a term to 'message' for each parsed word. Each
539  * term will be added both prefixed (if prefix_name is not NULL) and
540  * also unprefixed). */
541 notmuch_private_status_t
542 _notmuch_message_gen_terms (notmuch_message_t *message,
543                             const char *prefix_name,
544                             const char *text)
545 {
546     Xapian::TermGenerator *term_gen = message->notmuch->term_gen;
547
548     if (text == NULL)
549         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
550
551     term_gen->set_document (message->doc);
552
553     if (prefix_name) {
554         const char *prefix = _find_prefix (prefix_name);
555
556         term_gen->index_text (text, 1, prefix);
557     }
558
559     term_gen->index_text (text);
560
561     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
562 }
563
564 /* Remove a name:value term from 'message', (the actual term will be
565  * encoded by prefixing the value with a short prefix). See
566  * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
567  * names to prefix values.
568  *
569  * This change will not be reflected in the database until the next
570  * call to _notmuch_message_set_sync. */
571 notmuch_private_status_t
572 _notmuch_message_remove_term (notmuch_message_t *message,
573                               const char *prefix_name,
574                               const char *value)
575 {
576     char *term;
577
578     if (value == NULL)
579         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
580
581     term = talloc_asprintf (message, "%s%s",
582                             _find_prefix (prefix_name), value);
583
584     if (strlen (term) > NOTMUCH_TERM_MAX)
585         return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
586
587     try {
588         message->doc.remove_term (term);
589     } catch (const Xapian::InvalidArgumentError) {
590         /* We'll let the philosopher's try to wrestle with the
591          * question of whether failing to remove that which was not
592          * there in the first place is failure. For us, we'll silently
593          * consider it all good. */
594     }
595
596     talloc_free (term);
597
598     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
599 }
600
601 notmuch_status_t
602 notmuch_message_add_tag (notmuch_message_t *message, const char *tag)
603 {
604     notmuch_private_status_t status;
605
606     if (tag == NULL)
607         return NOTMUCH_STATUS_NULL_POINTER;
608
609     if (strlen (tag) > NOTMUCH_TAG_MAX)
610         return NOTMUCH_STATUS_TAG_TOO_LONG;
611
612     status = _notmuch_message_add_term (message, "tag", tag);
613     if (status) {
614         INTERNAL_ERROR ("_notmuch_message_add_term return unexpected value: %d\n",
615                         status);
616     }
617
618     if (! message->frozen)
619         _notmuch_message_sync (message);
620
621     return NOTMUCH_STATUS_SUCCESS;
622 }
623
624 notmuch_status_t
625 notmuch_message_remove_tag (notmuch_message_t *message, const char *tag)
626 {
627     notmuch_private_status_t status;
628
629     if (tag == NULL)
630         return NOTMUCH_STATUS_NULL_POINTER;
631
632     if (strlen (tag) > NOTMUCH_TAG_MAX)
633         return NOTMUCH_STATUS_TAG_TOO_LONG;
634
635     status = _notmuch_message_remove_term (message, "tag", tag);
636     if (status) {
637         INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
638                         status);
639     }
640
641     if (! message->frozen)
642         _notmuch_message_sync (message);
643
644     return NOTMUCH_STATUS_SUCCESS;
645 }
646
647 void
648 notmuch_message_remove_all_tags (notmuch_message_t *message)
649 {
650     notmuch_private_status_t status;
651     notmuch_tags_t *tags;
652     const char *tag;
653
654     for (tags = notmuch_message_get_tags (message);
655          notmuch_tags_has_more (tags);
656          notmuch_tags_advance (tags))
657     {
658         tag = notmuch_tags_get (tags);
659
660         status = _notmuch_message_remove_term (message, "tag", tag);
661         if (status) {
662             INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
663                             status);
664         }
665     }
666
667     if (! message->frozen)
668         _notmuch_message_sync (message);
669 }
670
671 void
672 notmuch_message_freeze (notmuch_message_t *message)
673 {
674     message->frozen++;
675 }
676
677 notmuch_status_t
678 notmuch_message_thaw (notmuch_message_t *message)
679 {
680     if (message->frozen > 0) {
681         message->frozen--;
682         if (message->frozen == 0)
683             _notmuch_message_sync (message);
684         return NOTMUCH_STATUS_SUCCESS;
685     } else {
686         return NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW;
687     }
688 }
689
690 void
691 notmuch_message_destroy (notmuch_message_t *message)
692 {
693     talloc_free (message);
694 }