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