]> git.notmuchmail.org Git - notmuch/blob - database.cc
d513b8552bf12bc1fa7752e3275002977f73acec
[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 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
32
33 /* These prefix values are specifically chosen to be compatible
34  * with sup, (http://sup.rubyforge.org), written by
35  * William Morgan <wmorgan-sup@masanjin.net>, and released
36  * under the GNU GPL v2.
37  */
38
39 typedef struct {
40     const char *name;
41     const char *prefix;
42 } prefix_t;
43
44 prefix_t BOOLEAN_PREFIX[] = {
45     { "type", "K" },
46     { "tag", "L" },
47     { "id", "Q" },
48     { "thread", "H" },
49     { "ref", "R" },
50     { "timestamp", "KTS" },
51 };
52
53 const char *
54 _find_prefix (const char *name)
55 {
56     unsigned int i;
57
58     for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX); i++)
59         if (strcmp (name, BOOLEAN_PREFIX[i].name) == 0)
60             return BOOLEAN_PREFIX[i].prefix;
61
62     fprintf (stderr, "Internal error: No prefix exists for '%s'\n", name);
63     exit (1);
64
65     return "";
66 }
67
68 const char *
69 notmuch_status_to_string (notmuch_status_t status)
70 {
71     switch (status) {
72     case NOTMUCH_STATUS_SUCCESS:
73         return "No error occurred";
74     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
75         return "A Xapian exception occurred";
76     case NOTMUCH_STATUS_FILE_ERROR:
77         return "Something went wrong trying to read or write a file";
78     case NOTMUCH_STATUS_FILE_NOT_EMAIL:
79         return "File is not an email";
80     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
81         return "Message ID is identical to a message in database";
82     case NOTMUCH_STATUS_NULL_POINTER:
83         return "Erroneous NULL pointer";
84     case NOTMUCH_STATUS_TAG_TOO_LONG:
85         return "Tag value is too long (exceeds NOTMUCH_TAG_MAX)";
86     default:
87     case NOTMUCH_STATUS_LAST_STATUS:
88         return "Unknown error status value";
89     }
90 }
91
92 /* XXX: We should drop this function and convert all callers to call
93  * _notmuch_message_add_term instead. */
94 static void
95 add_term (Xapian::Document doc,
96           const char *prefix_name,
97           const char *value)
98 {
99     const char *prefix;
100     char *term;
101
102     if (value == NULL)
103         return;
104
105     prefix = _find_prefix (prefix_name);
106
107     term = g_strdup_printf ("%s%s", prefix, value);
108
109     if (strlen (term) <= NOTMUCH_TERM_MAX)
110         doc.add_term (term);
111
112     g_free (term);
113 }
114
115 static void
116 find_doc_ids (notmuch_database_t *notmuch,
117               const char *prefix_name,
118               const char *value,
119               Xapian::PostingIterator *begin,
120               Xapian::PostingIterator *end)
121 {
122     Xapian::PostingIterator i;
123     char *term;
124
125     term = g_strdup_printf ("%s%s", _find_prefix (prefix_name), value);
126
127     *begin = notmuch->xapian_db->postlist_begin (term);
128
129     *end = notmuch->xapian_db->postlist_end (term);
130
131     free (term);
132 }
133
134 static notmuch_private_status_t
135 find_unique_doc_id (notmuch_database_t *notmuch,
136                     const char *prefix_name,
137                     const char *value,
138                     unsigned int *doc_id)
139 {
140     Xapian::PostingIterator i, end;
141
142     find_doc_ids (notmuch, prefix_name, value, &i, &end);
143
144     if (i == end) {
145         *doc_id = 0;
146         return NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
147     } else {
148         *doc_id = *i;
149         return NOTMUCH_PRIVATE_STATUS_SUCCESS;
150     }
151 }
152
153 static Xapian::Document
154 find_document_for_doc_id (notmuch_database_t *notmuch, unsigned doc_id)
155 {
156     return notmuch->xapian_db->get_document (doc_id);
157 }
158
159 static notmuch_private_status_t
160 find_unique_document (notmuch_database_t *notmuch,
161                       const char *prefix_name,
162                       const char *value,
163                       Xapian::Document *document,
164                       unsigned int *doc_id)
165 {
166     notmuch_private_status_t status;
167
168     status = find_unique_doc_id (notmuch, prefix_name, value, doc_id);
169
170     if (status) {
171         *document = Xapian::Document ();
172         return status;
173     }
174
175     *document = find_document_for_doc_id (notmuch, *doc_id);
176     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
177 }
178
179 static void
180 insert_thread_id (GHashTable *thread_ids, Xapian::Document doc)
181 {
182     string value_string;
183     const char *value, *id, *comma;
184
185     value_string = doc.get_value (NOTMUCH_VALUE_THREAD);
186     value = value_string.c_str();
187     if (strlen (value)) {
188         id = value;
189         while (*id) {
190             comma = strchr (id, ',');
191             if (comma == NULL)
192                 comma = id + strlen (id);
193             g_hash_table_insert (thread_ids,
194                                  strndup (id, comma - id), NULL);
195             id = comma;
196             if (*id)
197                 id++;
198         }
199     }
200 }
201
202 notmuch_message_t *
203 notmuch_database_find_message (notmuch_database_t *notmuch,
204                                const char *message_id)
205 {
206     notmuch_private_status_t status;
207     unsigned int doc_id;
208
209     status = find_unique_doc_id (notmuch, "id", message_id, &doc_id);
210
211     if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
212         return NULL;
213
214     return _notmuch_message_create (notmuch, notmuch, doc_id);
215 }
216
217 /* Return one or more thread_ids, (as a GPtrArray of strings), for the
218  * given message based on looking into the database for any messages
219  * referenced in parents, and also for any messages in the database
220  * referencing message_id.
221  *
222  * Caller should free all strings in the array and the array itself,
223  * (g_ptr_array_free) when done. */
224 static GPtrArray *
225 find_thread_ids (notmuch_database_t *notmuch,
226                  GPtrArray *parents,
227                  const char *message_id)
228 {
229     Xapian::PostingIterator child, children_end;
230     Xapian::Document doc;
231     GHashTable *thread_ids;
232     GList *keys, *l;
233     unsigned int i;
234     const char *parent_message_id;
235     GPtrArray *result;
236
237     thread_ids = g_hash_table_new_full (g_str_hash, g_str_equal,
238                                         free, NULL);
239
240     find_doc_ids (notmuch, "ref", message_id, &child, &children_end);
241     for ( ; child != children_end; child++) {
242         doc = find_document_for_doc_id (notmuch, *child);
243         insert_thread_id (thread_ids, doc);
244     }
245
246     for (i = 0; i < parents->len; i++) {
247         notmuch_message_t *parent;
248         notmuch_thread_ids_t *ids;
249
250         parent_message_id = (char *) g_ptr_array_index (parents, i);
251         parent = notmuch_database_find_message (notmuch, parent_message_id);
252         if (parent == NULL)
253             continue;
254
255         for (ids = notmuch_message_get_thread_ids (parent);
256              notmuch_thread_ids_has_more (ids);
257              notmuch_thread_ids_advance (ids))
258         {
259             const char *id;
260
261             id = notmuch_thread_ids_get (ids);
262             g_hash_table_insert (thread_ids, strdup (id), NULL);
263         }
264
265         notmuch_message_destroy (parent);
266     }
267
268     result = g_ptr_array_new ();
269
270     keys = g_hash_table_get_keys (thread_ids);
271     for (l = keys; l; l = l->next) {
272         char *id = (char *) l->data;
273         g_ptr_array_add (result, id);
274     }
275     g_list_free (keys);
276
277     /* We're done with the hash table, but we've taken the pointers to
278      * the allocated strings and put them into our result array, so
279      * tell the hash not to free them on its way out. */
280     g_hash_table_steal_all (thread_ids);
281     g_hash_table_unref (thread_ids);
282
283     return result;
284 }
285
286 /* Advance 'str' past any whitespace or RFC 822 comments. A comment is
287  * a (potentially nested) parenthesized sequence with '\' used to
288  * escape any character (including parentheses).
289  *
290  * If the sequence to be skipped continues to the end of the string,
291  * then 'str' will be left pointing at the final terminating '\0'
292  * character.
293  */
294 static void
295 skip_space_and_comments (const char **str)
296 {
297     const char *s;
298
299     s = *str;
300     while (*s && (isspace (*s) || *s == '(')) {
301         while (*s && isspace (*s))
302             s++;
303         if (*s == '(') {
304             int nesting = 1;
305             s++;
306             while (*s && nesting) {
307                 if (*s == '(')
308                     nesting++;
309                 else if (*s == ')')
310                     nesting--;
311                 else if (*s == '\\')
312                     if (*(s+1))
313                         s++;
314                 s++;
315             }
316         }
317     }
318
319     *str = s;
320 }
321
322 /* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
323  * comments, and the '<' and '>' delimeters.
324  *
325  * If not NULL, then *next will be made to point to the first character
326  * not parsed, (possibly pointing to the final '\0' terminator.
327  *
328  * Returns a newly allocated string which the caller should free()
329  * when done with it.
330  *
331  * Returns NULL if there is any error parsing the message-id. */
332 static char *
333 parse_message_id (const char *message_id, const char **next)
334 {
335     const char *s, *end;
336     char *result;
337
338     if (message_id == NULL)
339         return NULL;
340
341     s = message_id;
342
343     skip_space_and_comments (&s);
344
345     /* Skip any unstructured text as well. */
346     while (*s && *s != '<')
347         s++;
348
349     if (*s == '<') {
350         s++;
351     } else {
352         if (next)
353             *next = s;
354         return NULL;
355     }
356
357     skip_space_and_comments (&s);
358
359     end = s;
360     while (*end && *end != '>')
361         end++;
362     if (next) {
363         if (*end)
364             *next = end + 1;
365         else
366             *next = end;
367     }
368
369     if (end > s && *end == '>')
370         end--;
371     if (end <= s)
372         return NULL;
373
374     result = strndup (s, end - s + 1);
375
376     /* Finally, collapse any whitespace that is within the message-id
377      * itself. */
378     {
379         char *r;
380         int len;
381
382         for (r = result, len = strlen (r); *r; r++, len--)
383             if (*r == ' ' || *r == '\t')
384                 memmove (r, r+1, len);
385     }
386
387     return result;
388 }
389
390 /* Parse a References header value, putting a copy of each referenced
391  * message-id into 'array'. */
392 static void
393 parse_references (GPtrArray *array,
394                   const char *refs)
395 {
396     char *ref;
397
398     if (refs == NULL)
399         return;
400
401     while (*refs) {
402         ref = parse_message_id (refs, &refs);
403
404         if (ref)
405             g_ptr_array_add (array, ref);
406     }
407 }
408
409 char *
410 notmuch_database_default_path (void)
411 {
412     if (getenv ("NOTMUCH_BASE"))
413         return strdup (getenv ("NOTMUCH_BASE"));
414
415     return g_strdup_printf ("%s/mail", getenv ("HOME"));
416 }
417
418 notmuch_database_t *
419 notmuch_database_create (const char *path)
420 {
421     notmuch_database_t *notmuch = NULL;
422     char *notmuch_path = NULL;
423     struct stat st;
424     int err;
425     char *local_path = NULL;
426
427     if (path == NULL)
428         path = local_path = notmuch_database_default_path ();
429
430     err = stat (path, &st);
431     if (err) {
432         fprintf (stderr, "Error: Cannot create database at %s: %s.\n",
433                  path, strerror (errno));
434         goto DONE;
435     }
436
437     if (! S_ISDIR (st.st_mode)) {
438         fprintf (stderr, "Error: Cannot create database at %s: Not a directory.\n",
439                  path);
440         goto DONE;
441     }
442
443     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
444
445     err = mkdir (notmuch_path, 0755);
446
447     if (err) {
448         fprintf (stderr, "Error: Cannot create directory %s: %s.\n",
449                  notmuch_path, strerror (errno));
450         goto DONE;
451     }
452
453     notmuch = notmuch_database_open (path);
454
455   DONE:
456     if (notmuch_path)
457         free (notmuch_path);
458     if (local_path)
459         free (local_path);
460
461     return notmuch;
462 }
463
464 notmuch_database_t *
465 notmuch_database_open (const char *path)
466 {
467     notmuch_database_t *notmuch = NULL;
468     char *notmuch_path = NULL, *xapian_path = NULL;
469     struct stat st;
470     int err;
471     char *local_path = NULL;
472
473     if (path == NULL)
474         path = local_path = notmuch_database_default_path ();
475
476     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
477
478     err = stat (notmuch_path, &st);
479     if (err) {
480         fprintf (stderr, "Error opening database at %s: %s\n",
481                  notmuch_path, strerror (errno));
482         goto DONE;
483     }
484
485     xapian_path = g_strdup_printf ("%s/%s", notmuch_path, "xapian");
486
487     notmuch = talloc (NULL, notmuch_database_t);
488     notmuch->path = talloc_strdup (notmuch, path);
489
490     try {
491         notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
492                                                            Xapian::DB_CREATE_OR_OPEN);
493         notmuch->query_parser = new Xapian::QueryParser;
494         notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
495         notmuch->query_parser->set_database (*notmuch->xapian_db);
496         notmuch->query_parser->add_boolean_prefix ("id", _find_prefix ("id"));
497         notmuch->query_parser->add_boolean_prefix ("tag", _find_prefix ("tag"));
498         notmuch->query_parser->add_boolean_prefix ("type", _find_prefix ("type"));
499     } catch (const Xapian::Error &error) {
500         fprintf (stderr, "A Xapian exception occurred: %s\n",
501                  error.get_msg().c_str());
502     }
503     
504   DONE:
505     if (local_path)
506         free (local_path);
507     if (notmuch_path)
508         free (notmuch_path);
509     if (xapian_path)
510         free (xapian_path);
511
512     return notmuch;
513 }
514
515 void
516 notmuch_database_close (notmuch_database_t *notmuch)
517 {
518     delete notmuch->query_parser;
519     delete notmuch->xapian_db;
520     talloc_free (notmuch);
521 }
522
523 const char *
524 notmuch_database_get_path (notmuch_database_t *notmuch)
525 {
526     return notmuch->path;
527 }
528
529 notmuch_private_status_t
530 find_timestamp_document (notmuch_database_t *notmuch, const char *db_key,
531                          Xapian::Document *doc, unsigned int *doc_id)
532 {
533     return find_unique_document (notmuch, "timestamp", db_key, doc, doc_id);
534 }
535
536 /* We allow the user to use arbitrarily long keys for timestamps,
537  * (they're for filesystem paths after all, which have no limit we
538  * know about). But we have a term-length limit. So if we exceed that,
539  * we'll use the SHA-1 of the user's key as the actual key for
540  * constructing a database term.
541  *
542  * Caution: This function returns a newly allocated string which the
543  * caller should free() when finished.
544  */
545 static char *
546 timestamp_db_key (const char *key)
547 {
548     int term_len = strlen (_find_prefix ("timestamp")) + strlen (key);
549
550     if (term_len > NOTMUCH_TERM_MAX)
551         return notmuch_sha1_of_string (key);
552     else
553         return strdup (key);
554 }
555
556 notmuch_status_t
557 notmuch_database_set_timestamp (notmuch_database_t *notmuch,
558                                 const char *key, time_t timestamp)
559 {
560     Xapian::Document doc;
561     unsigned int doc_id;
562     notmuch_private_status_t status;
563     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
564     char *db_key = NULL;
565
566     db_key = timestamp_db_key (key);
567
568     try {
569         status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
570
571         doc.add_value (0, Xapian::sortable_serialise (timestamp));
572
573         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
574             char *term = talloc_asprintf (NULL, "%s%s",
575                                           _find_prefix ("timestamp"), db_key);
576             doc.add_term (term);
577             talloc_free (term);
578
579             notmuch->xapian_db->add_document (doc);
580         } else {
581             notmuch->xapian_db->replace_document (doc_id, doc);
582         }
583
584     } catch (Xapian::Error &error) {
585         fprintf (stderr, "A Xapian exception occurred: %s.\n",
586                  error.get_msg().c_str());
587         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
588     }
589
590     if (db_key)
591         free (db_key);
592
593     return ret;
594 }
595
596 time_t
597 notmuch_database_get_timestamp (notmuch_database_t *notmuch, const char *key)
598 {
599     Xapian::Document doc;
600     unsigned int doc_id;
601     notmuch_private_status_t status;
602     char *db_key = NULL;
603     time_t ret = 0;
604
605     db_key = timestamp_db_key (key);
606
607     try {
608         status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
609
610         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
611             goto DONE;
612
613         ret =  Xapian::sortable_unserialise (doc.get_value (0));
614     } catch (Xapian::Error &error) {
615         goto DONE;
616     }
617
618   DONE:
619     if (db_key)
620         free (db_key);
621
622     return ret;
623 }
624
625 notmuch_status_t
626 notmuch_database_add_message (notmuch_database_t *notmuch,
627                               const char *filename)
628 {
629     notmuch_message_file_t *message_file;
630     notmuch_message_t *message;
631     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
632
633     GPtrArray *parents, *thread_ids;
634
635     const char *refs, *in_reply_to, *date, *header;
636     const char *from, *to, *subject, *old_filename;
637     char *message_id;
638
639     unsigned int i;
640
641     message_file = notmuch_message_file_open (filename);
642     if (message_file == NULL) {
643         ret = NOTMUCH_STATUS_FILE_ERROR;
644         goto DONE;
645     }
646
647     notmuch_message_file_restrict_headers (message_file,
648                                            "date",
649                                            "from",
650                                            "in-reply-to",
651                                            "message-id",
652                                            "references",
653                                            "subject",
654                                            "to",
655                                            (char *) NULL);
656
657     try {
658         /* The first order of business is to find/create a message ID. */
659
660         header = notmuch_message_file_get_header (message_file, "message-id");
661         if (header) {
662             message_id = parse_message_id (header, NULL);
663             /* So the header value isn't RFC-compliant, but it's
664              * better than no message-id at all. */
665             if (message_id == NULL)
666                 message_id = xstrdup (header);
667         } else {
668             /* No message-id at all, let's generate one by taking a
669              * hash over the file's contents. */
670             char *sha1 = notmuch_sha1_of_file (filename);
671
672             /* If that failed too, something is really wrong. Give up. */
673             if (sha1 == NULL) {
674                 ret = NOTMUCH_STATUS_FILE_ERROR;
675                 goto DONE;
676             }
677
678             message_id = g_strdup_printf ("notmuch-sha1-%s", sha1);
679             free (sha1);
680         }
681
682         /* Now that we have a message ID, we get a message object,
683          * (which may or may not reference an existing document in the
684          * database). */
685
686         /* Use NULL for owner since we want to free this locally. */
687
688         /* XXX: This call can fail by either out-of-memory or an
689          * "impossible" Xapian exception. We should rewrite it to
690          * allow us to propagate the error status. */
691         message = _notmuch_message_create_for_message_id (NULL, notmuch,
692                                                           message_id);
693         if (message == NULL) {
694             fprintf (stderr, "Internal error. This shouldn't happen.\n\n");
695             fprintf (stderr, "I mean, it's possible you ran out of memory, but then this code path is still an internal error since it should have detected that and propagated the status value up the stack.\n");
696             exit (1);
697         }
698
699         /* Has a message previously been added with the same ID? */
700         old_filename = notmuch_message_get_filename (message);
701         if (old_filename && strlen (old_filename)) {
702             ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
703             goto DONE;
704         } else {
705             _notmuch_message_set_filename (message, filename);
706             _notmuch_message_add_term (message, "type", "mail");
707         }
708
709         /* Next, find the thread(s) to which this message belongs. */
710         parents = g_ptr_array_new ();
711
712         refs = notmuch_message_file_get_header (message_file, "references");
713         parse_references (parents, refs);
714
715         in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
716         parse_references (parents, in_reply_to);
717
718         for (i = 0; i < parents->len; i++)
719             _notmuch_message_add_term (message, "ref",
720                                        (char *) g_ptr_array_index (parents, i));
721
722         thread_ids = find_thread_ids (notmuch, parents, message_id);
723
724         free (message_id);
725
726         for (i = 0; i < parents->len; i++)
727             g_free (g_ptr_array_index (parents, i));
728         g_ptr_array_free (parents, TRUE);
729
730         if (thread_ids->len) {
731             unsigned int i;
732             GString *thread_id;
733             char *id;
734
735             for (i = 0; i < thread_ids->len; i++) {
736                 id = (char *) thread_ids->pdata[i];
737                 _notmuch_message_add_thread_id (message, id);
738                 if (i == 0)
739                     thread_id = g_string_new (id);
740                 else
741                     g_string_append_printf (thread_id, ",%s", id);
742
743                 free (id);
744             }
745             g_string_free (thread_id, TRUE);
746         } else {
747             _notmuch_message_ensure_thread_id (message);
748         }
749
750         g_ptr_array_free (thread_ids, TRUE);
751
752         date = notmuch_message_file_get_header (message_file, "date");
753         _notmuch_message_set_date (message, date);
754
755         from = notmuch_message_file_get_header (message_file, "from");
756         subject = notmuch_message_file_get_header (message_file, "subject");
757         to = notmuch_message_file_get_header (message_file, "to");
758
759         if (from == NULL &&
760             subject == NULL &&
761             to == NULL)
762         {
763             ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
764             goto DONE;
765         } else {
766             _notmuch_message_sync (message);
767         }
768     } catch (const Xapian::Error &error) {
769         fprintf (stderr, "A Xapian exception occurred: %s.\n",
770                  error.get_msg().c_str());
771         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
772         goto DONE;
773     }
774
775   DONE:
776     if (message)
777         notmuch_message_destroy (message);
778     if (message_file)
779         notmuch_message_file_close (message_file);
780
781     return ret;
782 }