]> git.notmuchmail.org Git - notmuch/blob - database.cc
database: Add new notmuch_database_find_message
[notmuch] / database.cc
1 /* database.cc - The database interfaces of the notmuch mail library
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 "database-private.h"
22
23 #include <iostream>
24
25 #include <xapian.h>
26
27 #include <glib.h> /* g_strdup_printf, g_free, GPtrArray, GHashTable */
28
29 using namespace std;
30
31 /* "128 bits of thread-id ought to be enough for anybody" */
32 #define NOTMUCH_THREAD_ID_BITS   128
33 #define NOTMUCH_THREAD_ID_DIGITS (NOTMUCH_THREAD_ID_BITS / 4)
34 typedef struct _thread_id {
35     char str[NOTMUCH_THREAD_ID_DIGITS + 1];
36 } thread_id_t;
37
38 static void
39 thread_id_generate (thread_id_t *thread_id)
40 {
41     static int seeded = 0;
42     FILE *dev_random;
43     uint32_t value;
44     char *s;
45     int i;
46
47     if (! seeded) {
48         dev_random = fopen ("/dev/random", "r");
49         if (dev_random == NULL) {
50             srand (time (NULL));
51         } else {
52             fread ((void *) &value, sizeof (value), 1, dev_random);
53             srand (value);
54             fclose (dev_random);
55         }
56         seeded = 1;
57     }
58
59     s = thread_id->str;
60     for (i = 0; i < NOTMUCH_THREAD_ID_DIGITS; i += 8) {
61         value = rand ();
62         sprintf (s, "%08x", value);
63         s += 8;
64     }
65 }
66
67 static void
68 add_term (Xapian::Document doc,
69           const char *prefix_name,
70           const char *value)
71 {
72     const char *prefix;
73     char *term;
74
75     if (value == NULL)
76         return;
77
78     prefix = _find_prefix (prefix_name);
79
80     term = g_strdup_printf ("%s%s", prefix, value);
81
82     if (strlen (term) <= NOTMUCH_TERM_MAX)
83         doc.add_term (term);
84
85     g_free (term);
86 }
87
88 static void
89 find_messages_by_term (Xapian::Database *db,
90                        const char *prefix_name,
91                        const char *value,
92                        Xapian::PostingIterator *begin,
93                        Xapian::PostingIterator *end)
94 {
95     Xapian::PostingIterator i;
96     char *term;
97
98     term = g_strdup_printf ("%s%s", _find_prefix (prefix_name), value);
99
100     *begin = db->postlist_begin (term);
101
102     if (end)
103         *end = db->postlist_end (term);
104
105     free (term);
106 }
107
108 Xapian::Document
109 find_message_by_docid (Xapian::Database *db, Xapian::docid docid)
110 {
111     return db->get_document (docid);
112 }
113
114 notmuch_message_t *
115 notmuch_database_find_message (notmuch_database_t *notmuch,
116                                const char *message_id)
117 {
118     Xapian::PostingIterator i, end;
119
120     find_messages_by_term (notmuch->xapian_db,
121                            "msgid", message_id, &i, &end);
122
123     if (i == end)
124         return NULL;
125
126     return _notmuch_message_create (notmuch, notmuch, *i);
127 }
128
129 /* Return one or more thread_ids, (as a GPtrArray of strings), for the
130  * given message based on looking into the database for any messages
131  * referenced in parents, and also for any messages in the database
132  * referencing message_id.
133  *
134  * Caller should free all strings in the array and the array itself,
135  * (g_ptr_array_free) when done. */
136 static GPtrArray *
137 find_thread_ids (notmuch_database_t *notmuch,
138                  GPtrArray *parents,
139                  const char *message_id)
140 {
141     Xapian::WritableDatabase *db = notmuch->xapian_db;
142     Xapian::PostingIterator child, children_end;
143     Xapian::Document doc;
144     GHashTable *thread_ids;
145     GList *keys, *l;
146     unsigned int i;
147     const char *parent_message_id;
148     GPtrArray *result;
149
150     thread_ids = g_hash_table_new_full (g_str_hash, g_str_equal,
151                                         free, NULL);
152
153     find_messages_by_term (db, "ref", message_id, &child, &children_end);
154     for ( ; child != children_end; child++) {
155         const char *thread_id;
156         doc = find_message_by_docid (db, *child);
157
158         thread_id = doc.get_value (NOTMUCH_VALUE_THREAD).c_str ();
159         if (strlen (thread_id) == 0) {
160             fprintf (stderr, "Database error: Message with doc_id %u has empty thread-id value (value index %d)\n",
161                      *child, NOTMUCH_VALUE_THREAD);
162         } else {
163             g_hash_table_insert (thread_ids, strdup (thread_id), NULL);
164         }
165     }
166
167     for (i = 0; i < parents->len; i++) {
168         notmuch_message_t *parent;
169         notmuch_thread_ids_t *ids;
170
171         parent_message_id = (char *) g_ptr_array_index (parents, i);
172         parent = notmuch_database_find_message (notmuch, parent_message_id);
173         if (parent == NULL)
174             continue;
175
176         for (ids = notmuch_message_get_thread_ids (parent);
177              notmuch_thread_ids_has_more (ids);
178              notmuch_thread_ids_advance (ids))
179         {
180             const char *id;
181
182             id = notmuch_thread_ids_get (ids);
183             g_hash_table_insert (thread_ids, strdup (id), NULL);
184         }
185
186         notmuch_message_destroy (parent);
187     }
188
189     result = g_ptr_array_new ();
190
191     keys = g_hash_table_get_keys (thread_ids);
192     for (l = keys; l; l = l->next) {
193         char *id = (char *) l->data;
194         g_ptr_array_add (result, id);
195     }
196     g_list_free (keys);
197
198     /* We're done with the hash table, but we've taken the pointers to
199      * the allocated strings and put them into our result array, so
200      * tell the hash not to free them on its way out. */
201     g_hash_table_steal_all (thread_ids);
202     g_hash_table_unref (thread_ids);
203
204     return result;
205 }
206
207 /* Advance 'str' past any whitespace or RFC 822 comments. A comment is
208  * a (potentially nested) parenthesized sequence with '\' used to
209  * escape any character (including parentheses).
210  *
211  * If the sequence to be skipped continues to the end of the string,
212  * then 'str' will be left pointing at the final terminating '\0'
213  * character.
214  */
215 static void
216 skip_space_and_comments (const char **str)
217 {
218     const char *s;
219
220     s = *str;
221     while (*s && (isspace (*s) || *s == '(')) {
222         while (*s && isspace (*s))
223             s++;
224         if (*s == '(') {
225             int nesting = 1;
226             s++;
227             while (*s && nesting) {
228                 if (*s == '(')
229                     nesting++;
230                 else if (*s == ')')
231                     nesting--;
232                 else if (*s == '\\')
233                     if (*(s+1))
234                         s++;
235                 s++;
236             }
237         }
238     }
239
240     *str = s;
241 }
242
243 /* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
244  * comments, and the '<' and '>' delimeters.
245  *
246  * If not NULL, then *next will be made to point to the first character
247  * not parsed, (possibly pointing to the final '\0' terminator.
248  *
249  * Returns a newly allocated string which the caller should free()
250  * when done with it.
251  *
252  * Returns NULL if there is any error parsing the message-id. */
253 static char *
254 parse_message_id (const char *message_id, const char **next)
255 {
256     const char *s, *end;
257     char *result;
258
259     if (message_id == NULL)
260         return NULL;
261
262     s = message_id;
263
264     skip_space_and_comments (&s);
265
266     /* Skip any unstructured text as well. */
267     while (*s && *s != '<')
268         s++;
269
270     if (*s == '<') {
271         s++;
272     } else {
273         if (next)
274             *next = s;
275         return NULL;
276     }
277
278     skip_space_and_comments (&s);
279
280     end = s;
281     while (*end && *end != '>')
282         end++;
283     if (next) {
284         if (*end)
285             *next = end + 1;
286         else
287             *next = end;
288     }
289
290     if (end > s && *end == '>')
291         end--;
292     if (end <= s)
293         return NULL;
294
295     result = strndup (s, end - s + 1);
296
297     /* Finally, collapse any whitespace that is within the message-id
298      * itself. */
299     {
300         char *r;
301         int len;
302
303         for (r = result, len = strlen (r); *r; r++, len--)
304             if (*r == ' ' || *r == '\t')
305                 memmove (r, r+1, len);
306     }
307
308     return result;
309 }
310
311 /* Parse a References header value, putting a copy of each referenced
312  * message-id into 'array'. */
313 static void
314 parse_references (GPtrArray *array,
315                   const char *refs)
316 {
317     char *ref;
318
319     if (refs == NULL)
320         return;
321
322     while (*refs) {
323         ref = parse_message_id (refs, &refs);
324
325         if (ref)
326             g_ptr_array_add (array, ref);
327     }
328 }
329
330 char *
331 notmuch_database_default_path (void)
332 {
333     if (getenv ("NOTMUCH_BASE"))
334         return strdup (getenv ("NOTMUCH_BASE"));
335
336     return g_strdup_printf ("%s/mail", getenv ("HOME"));
337 }
338
339 notmuch_database_t *
340 notmuch_database_create (const char *path)
341 {
342     notmuch_database_t *notmuch = NULL;
343     char *notmuch_path = NULL;
344     struct stat st;
345     int err;
346     char *local_path = NULL;
347
348     if (path == NULL)
349         path = local_path = notmuch_database_default_path ();
350
351     err = stat (path, &st);
352     if (err) {
353         fprintf (stderr, "Error: Cannot create database at %s: %s.\n",
354                  path, strerror (errno));
355         goto DONE;
356     }
357
358     if (! S_ISDIR (st.st_mode)) {
359         fprintf (stderr, "Error: Cannot create database at %s: Not a directory.\n",
360                  path);
361         goto DONE;
362     }
363
364     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
365
366     err = mkdir (notmuch_path, 0755);
367
368     if (err) {
369         fprintf (stderr, "Error: Cannot create directory %s: %s.\n",
370                  notmuch_path, strerror (errno));
371         goto DONE;
372     }
373
374     notmuch = notmuch_database_open (path);
375
376   DONE:
377     if (notmuch_path)
378         free (notmuch_path);
379     if (local_path)
380         free (local_path);
381
382     return notmuch;
383 }
384
385 notmuch_database_t *
386 notmuch_database_open (const char *path)
387 {
388     notmuch_database_t *notmuch = NULL;
389     char *notmuch_path = NULL, *xapian_path = NULL;
390     struct stat st;
391     int err;
392     char *local_path = NULL;
393
394     if (path == NULL)
395         path = local_path = notmuch_database_default_path ();
396
397     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
398
399     err = stat (notmuch_path, &st);
400     if (err) {
401         fprintf (stderr, "Error opening database at %s: %s\n",
402                  notmuch_path, strerror (errno));
403         goto DONE;
404     }
405
406     xapian_path = g_strdup_printf ("%s/%s", notmuch_path, "xapian");
407
408     notmuch = talloc (NULL, notmuch_database_t);
409     notmuch->path = talloc_strdup (notmuch, path);
410
411     try {
412         notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
413                                                            Xapian::DB_CREATE_OR_OPEN);
414         notmuch->query_parser = new Xapian::QueryParser;
415         notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
416         notmuch->query_parser->set_database (*notmuch->xapian_db);
417     } catch (const Xapian::Error &error) {
418         fprintf (stderr, "A Xapian exception occurred: %s\n",
419                  error.get_msg().c_str());
420     }
421     
422   DONE:
423     if (local_path)
424         free (local_path);
425     if (notmuch_path)
426         free (notmuch_path);
427     if (xapian_path)
428         free (xapian_path);
429
430     return notmuch;
431 }
432
433 void
434 notmuch_database_close (notmuch_database_t *notmuch)
435 {
436     delete notmuch->query_parser;
437     delete notmuch->xapian_db;
438     talloc_free (notmuch);
439 }
440
441 const char *
442 notmuch_database_get_path (notmuch_database_t *notmuch)
443 {
444     return notmuch->path;
445 }
446
447 notmuch_status_t
448 notmuch_database_add_message (notmuch_database_t *notmuch,
449                               const char *filename)
450 {
451     Xapian::WritableDatabase *db = notmuch->xapian_db;
452     Xapian::Document doc;
453     notmuch_message_file_t *message;
454
455     GPtrArray *parents, *thread_ids;
456
457     const char *refs, *in_reply_to, *date, *header;
458     const char *from, *to, *subject;
459     char *message_id;
460
461     time_t time_value;
462     unsigned int i;
463
464     message = notmuch_message_file_open (filename);
465
466     notmuch_message_file_restrict_headers (message,
467                                            "date",
468                                            "from",
469                                            "in-reply-to",
470                                            "message-id",
471                                            "references",
472                                            "subject",
473                                            (char *) NULL);
474
475     try {
476         doc.set_data (filename);
477
478         add_term (doc, "type", "mail");
479
480         parents = g_ptr_array_new ();
481
482         refs = notmuch_message_file_get_header (message, "references");
483         parse_references (parents, refs);
484
485         in_reply_to = notmuch_message_file_get_header (message, "in-reply-to");
486         parse_references (parents, in_reply_to);
487
488         for (i = 0; i < parents->len; i++)
489             add_term (doc, "ref", (char *) g_ptr_array_index (parents, i));
490
491         header = notmuch_message_file_get_header (message, "message-id");
492         if (header) {
493             message_id = parse_message_id (header, NULL);
494             /* So the header value isn't RFC-compliant, but it's
495              * better than no message-id at all. */
496             if (message_id == NULL)
497                 message_id = xstrdup (header);
498         } else {
499             /* XXX: Should generate a message_id here, (such as a SHA1
500              * sum of the message itself) */
501             message_id = NULL;
502         }
503
504         thread_ids = find_thread_ids (notmuch, parents, message_id);
505
506         for (i = 0; i < parents->len; i++)
507             g_free (g_ptr_array_index (parents, i));
508         g_ptr_array_free (parents, TRUE);
509         if (message_id) {
510             add_term (doc, "msgid", message_id);
511             doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id);
512         }
513
514         if (thread_ids->len) {
515             unsigned int i;
516             GString *thread_id;
517             char *id;
518
519             for (i = 0; i < thread_ids->len; i++) {
520                 id = (char *) thread_ids->pdata[i];
521                 add_term (doc, "thread", id);
522                 if (i == 0)
523                     thread_id = g_string_new (id);
524                 else
525                     g_string_append_printf (thread_id, ",%s", id);
526
527                 free (id);
528             }
529             doc.add_value (NOTMUCH_VALUE_THREAD, thread_id->str);
530             g_string_free (thread_id, TRUE);
531         } else if (message_id) {
532             /* If not part of any existing thread, generate a new thread_id. */
533             thread_id_t thread_id;
534
535             thread_id_generate (&thread_id);
536             add_term (doc, "thread", thread_id.str);
537             doc.add_value (NOTMUCH_VALUE_THREAD, thread_id.str);
538         }
539
540         g_ptr_array_free (thread_ids, TRUE);
541
542         free (message_id);
543
544         date = notmuch_message_file_get_header (message, "date");
545         time_value = notmuch_parse_date (date, NULL);
546
547         doc.add_value (NOTMUCH_VALUE_DATE,
548                        Xapian::sortable_serialise (time_value));
549
550         from = notmuch_message_file_get_header (message, "from");
551         subject = notmuch_message_file_get_header (message, "subject");
552         to = notmuch_message_file_get_header (message, "to");
553
554         if (from == NULL &&
555             subject == NULL &&
556             to == NULL)
557         {
558             notmuch_message_file_close (message);
559             return NOTMUCH_STATUS_FILE_NOT_EMAIL;
560         } else {
561             db->add_document (doc);
562         }
563     } catch (const Xapian::Error &error) {
564         fprintf (stderr, "A Xapian exception occurred: %s.\n",
565                  error.get_msg().c_str());
566         return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
567     }
568
569     notmuch_message_file_close (message);
570
571     return NOTMUCH_STATUS_SUCCESS;
572 }