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