]> git.notmuchmail.org Git - notmuch/blob - message.cc
21befc20ab0299936b78d374f1cdcd89209bccb6
[notmuch] / 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 <xapian.h>
25
26 struct _notmuch_message {
27     notmuch_database_t *notmuch;
28     Xapian::docid doc_id;
29     char *message_id;
30     char *filename;
31     Xapian::Document doc;
32 };
33
34 struct _notmuch_tags {
35     Xapian::TermIterator iterator;
36     Xapian::TermIterator iterator_end;
37 };
38
39 struct _notmuch_thread_ids {
40     char *current;
41     char *next;
42 };
43
44 /* "128 bits of thread-id ought to be enough for anybody" */
45 #define NOTMUCH_THREAD_ID_BITS   128
46 #define NOTMUCH_THREAD_ID_DIGITS (NOTMUCH_THREAD_ID_BITS / 4)
47 typedef struct _thread_id {
48     char str[NOTMUCH_THREAD_ID_DIGITS + 1];
49 } thread_id_t;
50
51 /* We end up having to call the destructor explicitly because we had
52  * to use "placement new" in order to initialize C++ objects within a
53  * block that we allocated with talloc. So C++ is making talloc
54  * slightly less simple to use, (we wouldn't need
55  * talloc_set_destructor at all otherwise).
56  */
57 static int
58 _notmuch_message_destructor (notmuch_message_t *message)
59 {
60     message->doc.~Document ();
61
62     return 0;
63 }
64
65 /* Create a new notmuch_message_t object for an existing document in
66  * the database.
67  *
68  * Here, 'talloc owner' is an optional talloc context to which the new
69  * message will belong. This allows for the caller to not bother
70  * calling notmuch_message_destroy on the message, and no that all
71  * memory will be reclaimed with 'talloc_owner' is free. The caller
72  * still can call notmuch_message_destroy when finished with the
73  * message if desired.
74  *
75  * The 'talloc_owner' argument can also be NULL, in which case the
76  * caller *is* responsible for calling notmuch_message_destroy.
77  *
78  * If no document exists in the database with document ID of 'doc_id'
79  * then this function returns NULL.
80  */
81 notmuch_message_t *
82 _notmuch_message_create (const void *talloc_owner,
83                          notmuch_database_t *notmuch,
84                          unsigned int doc_id)
85 {
86     notmuch_message_t *message;
87
88     message = talloc (talloc_owner, notmuch_message_t);
89     if (unlikely (message == NULL))
90         return NULL;
91
92     message->notmuch = notmuch;
93     message->doc_id = doc_id;
94     message->message_id = NULL; /* lazily created */
95     message->filename = NULL; /* lazily created */
96     new (&message->doc) Xapian::Document;
97
98     talloc_set_destructor (message, _notmuch_message_destructor);
99
100     try {
101         message->doc = notmuch->xapian_db->get_document (doc_id);
102     } catch (const Xapian::DocNotFoundError &error) {
103         talloc_free (message);
104         return NULL;
105     }
106
107     return message;
108 }
109
110 /* Create a new notmuch_message_t object for a specific message ID,
111  * (which may or may not already exist in the databas).
112  *
113  * Here, 'talloc owner' is an optional talloc context to which the new
114  * message will belong. This allows for the caller to not bother
115  * calling notmuch_message_destroy on the message, and no that all
116  * memory will be reclaimed with 'talloc_owner' is free. The caller
117  * still can call notmuch_message_destroy when finished with the
118  * message if desired.
119  *
120  * The 'talloc_owner' argument can also be NULL, in which case the
121  * caller *is* responsible for calling notmuch_message_destroy.
122  *
123  * If there is already a document with message ID 'message_id' in the
124  * database, then the returned message can be used to query/modify the
125  * document. Otherwise, a new document will be inserted into the
126  * database before this function returns;
127  */
128 notmuch_message_t *
129 _notmuch_message_create_for_message_id (const void *talloc_owner,
130                                         notmuch_database_t *notmuch,
131                                         const char *message_id)
132 {
133     notmuch_message_t *message;
134     Xapian::Document doc;
135     unsigned int doc_id;
136     char *term;
137
138     message = notmuch_database_find_message (notmuch, message_id);
139     if (message)
140         return talloc_steal (talloc_owner, message);
141
142     term = talloc_asprintf (NULL, "%s%s",
143                             _find_prefix ("msgid"), message_id);
144     doc.add_term (term);
145     talloc_free (term);
146
147     doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id);
148
149     doc_id = notmuch->xapian_db->add_document (doc);
150
151     return _notmuch_message_create (talloc_owner, notmuch, doc_id);
152 }
153
154 const char *
155 notmuch_message_get_message_id (notmuch_message_t *message)
156 {
157     Xapian::TermIterator i;
158
159     if (message->message_id)
160         return message->message_id;
161
162     i = message->doc.termlist_begin ();
163     i.skip_to (_find_prefix ("msgid"));
164
165     if (i == message->doc.termlist_end ()) {
166         fprintf (stderr, "Internal error: Message with document ID of %d has no message ID.\n",
167                  message->doc_id);
168         exit (1);
169     }
170
171     message->message_id = talloc_strdup (message, (*i).c_str () + 1);
172     return message->message_id;
173 }
174
175 /* Set the filename for 'message' to 'filename'.
176  *
177  * XXX: We should still figure out what we want to do for multiple
178  * files with identical message IDs. We will probably want to store a
179  * list of filenames here, (so that this will be "add_filename"
180  * instead of "set_filename"). Which would make this very similar to
181  * add_thread_ids.
182  *
183  * This change will not be reflected in the database until the next
184  * call to _notmuch_message_set_sync. */
185 void
186 _notmuch_message_set_filename (notmuch_message_t *message,
187                                const char *filename)
188 {
189     if (message->filename)
190         talloc_free (message->filename);
191     message->doc.set_data (filename);
192 }
193
194 const char *
195 notmuch_message_get_filename (notmuch_message_t *message)
196 {
197     std::string filename_str;
198
199     if (message->filename)
200         return message->filename;
201
202     filename_str = message->doc.get_data ();
203     message->filename = talloc_strdup (message, filename_str.c_str ());
204
205     return message->filename;
206 }
207
208 /* We end up having to call the destructors explicitly because we had
209  * to use "placement new" in order to initialize C++ objects within a
210  * block that we allocated with talloc. So C++ is making talloc
211  * slightly less simple to use, (we wouldn't need
212  * talloc_set_destructor at all otherwise).
213  */
214 static int
215 _notmuch_tags_destructor (notmuch_tags_t *tags)
216 {
217     tags->iterator.~TermIterator ();
218     tags->iterator_end.~TermIterator ();
219
220     return 0;
221 }
222
223 notmuch_tags_t *
224 notmuch_message_get_tags (notmuch_message_t *message)
225 {
226     notmuch_tags_t *tags;
227
228     tags = talloc (message, notmuch_tags_t);
229     if (unlikely (tags == NULL))
230         return NULL;
231
232     new (&tags->iterator) Xapian::TermIterator;
233     new (&tags->iterator_end) Xapian::TermIterator;
234
235     talloc_set_destructor (tags, _notmuch_tags_destructor);
236
237     tags->iterator = message->doc.termlist_begin ();
238     tags->iterator.skip_to (_find_prefix ("tag"));
239     tags->iterator_end = message->doc.termlist_end ();
240
241     return tags;
242 }
243
244 notmuch_thread_ids_t *
245 notmuch_message_get_thread_ids (notmuch_message_t *message)
246 {
247     notmuch_thread_ids_t *thread_ids;
248     std::string id_str;
249
250     thread_ids = talloc (message, notmuch_thread_ids_t);
251     if (unlikely (thread_ids == NULL))
252         return NULL;
253
254     id_str = message->doc.get_value (NOTMUCH_VALUE_THREAD);
255     thread_ids->next = talloc_strdup (message, id_str.c_str ());
256
257     /* Initialize thread_ids->current and terminate first ID. */
258     notmuch_thread_ids_advance (thread_ids);
259
260     return thread_ids;
261 }
262
263 void
264 _notmuch_message_set_date (notmuch_message_t *message,
265                            const char *date)
266 {
267     time_t time_value;
268
269     time_value = notmuch_parse_date (date, NULL);
270
271     message->doc.add_value (NOTMUCH_VALUE_DATE,
272                             Xapian::sortable_serialise (time_value));
273 }
274
275 void
276 _notmuch_message_add_thread_id (notmuch_message_t *message,
277                                 const char *thread_id)
278 {
279     std::string id_str;
280
281     _notmuch_message_add_term (message, "thread", thread_id);
282
283     id_str = message->doc.get_value (NOTMUCH_VALUE_THREAD);
284
285     if (id_str.empty ()) {
286         message->doc.add_value (NOTMUCH_VALUE_THREAD, thread_id);
287     } else {
288         size_t pos;
289
290         /* Think about using a hash here if there's any performance
291          * problem. */
292         pos = id_str.find (thread_id);
293         if (pos == std::string::npos) {
294             id_str.append (",");
295             id_str.append (thread_id);
296             message->doc.add_value (NOTMUCH_VALUE_THREAD, id_str);
297         }
298     }
299 }
300
301 static void
302 thread_id_generate (thread_id_t *thread_id)
303 {
304     static int seeded = 0;
305     FILE *dev_random;
306     uint32_t value;
307     char *s;
308     int i;
309
310     if (! seeded) {
311         dev_random = fopen ("/dev/random", "r");
312         if (dev_random == NULL) {
313             srand (time (NULL));
314         } else {
315             fread ((void *) &value, sizeof (value), 1, dev_random);
316             srand (value);
317             fclose (dev_random);
318         }
319         seeded = 1;
320     }
321
322     s = thread_id->str;
323     for (i = 0; i < NOTMUCH_THREAD_ID_DIGITS; i += 8) {
324         value = rand ();
325         sprintf (s, "%08x", value);
326         s += 8;
327     }
328 }
329
330 void
331 _notmuch_message_ensure_thread_id (notmuch_message_t *message)
332 {
333     /* If not part of any existing thread, generate a new thread_id. */
334     thread_id_t thread_id;
335
336     thread_id_generate (&thread_id);
337     _notmuch_message_add_term (message, "thread", thread_id.str);
338     message->doc.add_value (NOTMUCH_VALUE_THREAD, thread_id.str);
339 }
340
341 /* Synchronize changes made to message->doc out into the database. */
342 void
343 _notmuch_message_sync (notmuch_message_t *message)
344 {
345     Xapian::WritableDatabase *db = message->notmuch->xapian_db;
346
347     db->replace_document (message->doc_id, message->doc);
348 }
349
350 /* Add a name:value term to 'message', (the actual term will be
351  * encoded by prefixing the value with a short prefix). See
352  * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
353  * names to prefix values.
354  *
355  * This change will not be reflected in the database until the next
356  * call to _notmuch_message_set_sync. */
357 notmuch_private_status_t
358 _notmuch_message_add_term (notmuch_message_t *message,
359                            const char *prefix_name,
360                            const char *value)
361 {
362
363     char *term;
364
365     if (value == NULL)
366         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
367
368     term = talloc_asprintf (message, "%s%s",
369                             _find_prefix (prefix_name), value);
370
371     if (strlen (term) > NOTMUCH_TERM_MAX)
372         return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
373
374     message->doc.add_term (term);
375
376     talloc_free (term);
377
378     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
379 }
380
381 /* Remove a name:value term from 'message', (the actual term will be
382  * encoded by prefixing the value with a short prefix). See
383  * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
384  * names to prefix values.
385  *
386  * This change will not be reflected in the database until the next
387  * call to _notmuch_message_set_sync. */
388 notmuch_private_status_t
389 _notmuch_message_remove_term (notmuch_message_t *message,
390                               const char *prefix_name,
391                               const char *value)
392 {
393     char *term;
394
395     if (value == NULL)
396         return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
397
398     term = talloc_asprintf (message, "%s%s",
399                             _find_prefix (prefix_name), value);
400
401     if (strlen (term) > NOTMUCH_TERM_MAX)
402         return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
403
404     message->doc.remove_term (term);
405
406     talloc_free (term);
407
408     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
409 }
410
411 notmuch_status_t
412 notmuch_message_add_tag (notmuch_message_t *message, const char *tag)
413 {
414     notmuch_private_status_t status;
415
416     if (tag == NULL)
417         return NOTMUCH_STATUS_NULL_POINTER;
418
419     if (strlen (tag) > NOTMUCH_TAG_MAX)
420         return NOTMUCH_STATUS_TAG_TOO_LONG;
421
422     status = _notmuch_message_add_term (message, "tag", tag);
423     if (status) {
424         fprintf (stderr, "Internal error: _notmuch_message_add_term return unexpected value: %d\n",
425                  status);
426         exit (1);
427     }
428
429     _notmuch_message_sync (message);
430
431     return NOTMUCH_STATUS_SUCCESS;
432 }
433
434 notmuch_status_t
435 notmuch_message_remove_tag (notmuch_message_t *message, const char *tag)
436 {
437     notmuch_private_status_t status;
438
439     if (tag == NULL)
440         return NOTMUCH_STATUS_NULL_POINTER;
441
442     if (strlen (tag) > NOTMUCH_TAG_MAX)
443         return NOTMUCH_STATUS_TAG_TOO_LONG;
444
445     status = _notmuch_message_remove_term (message, "tag", tag);
446     if (status) {
447         fprintf (stderr, "Internal error: _notmuch_message_remove_term return unexpected value: %d\n",
448                  status);
449         exit (1);
450     }
451
452     _notmuch_message_sync (message);
453
454     return NOTMUCH_STATUS_SUCCESS;
455 }
456
457 void
458 notmuch_message_destroy (notmuch_message_t *message)
459 {
460     talloc_free (message);
461 }
462
463 notmuch_bool_t
464 notmuch_tags_has_more (notmuch_tags_t *tags)
465 {
466     std::string s;
467
468     if (tags->iterator == tags->iterator_end)
469         return FALSE;
470
471     s = *tags->iterator;
472     if (! s.empty () && s[0] == 'L')
473         return TRUE;
474     else
475         return FALSE;
476 }
477
478 const char *
479 notmuch_tags_get (notmuch_tags_t *tags)
480 {
481     return talloc_strdup (tags, (*tags->iterator).c_str () + 1);
482 }
483
484 void
485 notmuch_tags_advance (notmuch_tags_t *tags)
486 {
487     tags->iterator++;
488 }
489
490 void
491 notmuch_tags_destroy (notmuch_tags_t *tags)
492 {
493     talloc_free (tags);
494 }
495
496 notmuch_bool_t
497 notmuch_thread_ids_has_more (notmuch_thread_ids_t *thread_ids)
498 {
499     if (thread_ids->current == NULL || *thread_ids->current == '\0')
500         return FALSE;
501     else
502         return TRUE;
503 }
504
505 const char *
506 notmuch_thread_ids_get (notmuch_thread_ids_t *thread_ids)
507 {
508     return thread_ids->current;
509 }
510
511 void
512 notmuch_thread_ids_advance (notmuch_thread_ids_t *thread_ids)
513 {
514     thread_ids->current = strsep (&thread_ids->next, ",");
515 }
516
517 void
518 notmuch_thread_ids_destroy (notmuch_thread_ids_t *thread_ids)
519 {
520     talloc_free (thread_ids);
521 }