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