]> git.notmuchmail.org Git - notmuch/blob - database.cc
c470cc34977c17beff7d152f78c5f1ac975cefd2
[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     { "msgid", "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, "msgid", 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     } catch (const Xapian::Error &error) {
497         fprintf (stderr, "A Xapian exception occurred: %s\n",
498                  error.get_msg().c_str());
499     }
500     
501   DONE:
502     if (local_path)
503         free (local_path);
504     if (notmuch_path)
505         free (notmuch_path);
506     if (xapian_path)
507         free (xapian_path);
508
509     return notmuch;
510 }
511
512 void
513 notmuch_database_close (notmuch_database_t *notmuch)
514 {
515     delete notmuch->query_parser;
516     delete notmuch->xapian_db;
517     talloc_free (notmuch);
518 }
519
520 const char *
521 notmuch_database_get_path (notmuch_database_t *notmuch)
522 {
523     return notmuch->path;
524 }
525
526 notmuch_private_status_t
527 find_timestamp_document (notmuch_database_t *notmuch, const char *db_key,
528                          Xapian::Document *doc, unsigned int *doc_id)
529 {
530     return find_unique_document (notmuch, "timestamp", db_key, doc, doc_id);
531 }
532
533 /* We allow the user to use arbitrarily long keys for timestamps,
534  * (they're for filesystem paths after all, which have no limit we
535  * know about). But we have a term-length limit. So if we exceed that,
536  * we'll use the SHA-1 of the user's key as the actual key for
537  * constructing a database term.
538  *
539  * Caution: This function returns a newly allocated string which the
540  * caller should free() when finished.
541  */
542 static char *
543 timestamp_db_key (const char *key)
544 {
545     if (strlen (key) + 1 > NOTMUCH_TERM_MAX) {
546         return notmuch_sha1_of_string (key);
547     } else {
548         return strdup (key);
549     }
550 }
551
552 notmuch_status_t
553 notmuch_database_set_timestamp (notmuch_database_t *notmuch,
554                                 const char *key, time_t timestamp)
555 {
556     Xapian::Document doc;
557     unsigned int doc_id;
558     notmuch_private_status_t status;
559     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
560     char *db_key = NULL;
561
562     db_key = timestamp_db_key (key);
563
564     try {
565         status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
566
567         doc.add_value (0, Xapian::sortable_serialise (timestamp));
568
569         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
570             char *term = talloc_asprintf (NULL, "%s%s",
571                                           _find_prefix ("timestamp"), db_key);
572             doc.add_term (term);
573             talloc_free (term);
574
575             notmuch->xapian_db->add_document (doc);
576         } else {
577             notmuch->xapian_db->replace_document (doc_id, doc);
578         }
579
580     } catch (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     }
585
586     if (db_key)
587         free (db_key);
588
589     return ret;
590 }
591
592 time_t
593 notmuch_database_get_timestamp (notmuch_database_t *notmuch, const char *key)
594 {
595     Xapian::Document doc;
596     unsigned int doc_id;
597     notmuch_private_status_t status;
598     char *db_key = NULL;
599     time_t ret = 0;
600
601     db_key = timestamp_db_key (key);
602
603     try {
604         status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
605
606         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
607             goto DONE;
608
609         ret =  Xapian::sortable_unserialise (doc.get_value (0));
610     } catch (Xapian::Error &error) {
611         goto DONE;
612     }
613
614   DONE:
615     if (db_key)
616         free (db_key);
617
618     return ret;
619 }
620
621 notmuch_status_t
622 notmuch_database_add_message (notmuch_database_t *notmuch,
623                               const char *filename)
624 {
625     notmuch_message_file_t *message_file;
626     notmuch_message_t *message;
627     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
628
629     GPtrArray *parents, *thread_ids;
630
631     const char *refs, *in_reply_to, *date, *header;
632     const char *from, *to, *subject, *old_filename;
633     char *message_id;
634
635     unsigned int i;
636
637     message_file = notmuch_message_file_open (filename);
638     if (message_file == NULL) {
639         ret = NOTMUCH_STATUS_FILE_ERROR;
640         goto DONE;
641     }
642
643     notmuch_message_file_restrict_headers (message_file,
644                                            "date",
645                                            "from",
646                                            "in-reply-to",
647                                            "message-id",
648                                            "references",
649                                            "subject",
650                                            "to",
651                                            (char *) NULL);
652
653     try {
654         /* The first order of business is to find/create a message ID. */
655
656         header = notmuch_message_file_get_header (message_file, "message-id");
657         if (header) {
658             message_id = parse_message_id (header, NULL);
659             /* So the header value isn't RFC-compliant, but it's
660              * better than no message-id at all. */
661             if (message_id == NULL)
662                 message_id = xstrdup (header);
663         } else {
664             /* No message-id at all, let's generate one by taking a
665              * hash over the file's contents. */
666             char *sha1 = notmuch_sha1_of_file (filename);
667
668             /* If that failed too, something is really wrong. Give up. */
669             if (sha1 == NULL) {
670                 ret = NOTMUCH_STATUS_FILE_ERROR;
671                 goto DONE;
672             }
673
674             message_id = g_strdup_printf ("notmuch-sha1-%s", sha1);
675             free (sha1);
676         }
677
678         /* Now that we have a message ID, we get a message object,
679          * (which may or may not reference an existing document in the
680          * database). */
681
682         /* Use NULL for owner since we want to free this locally. */
683
684         /* XXX: This call can fail by either out-of-memory or an
685          * "impossible" Xapian exception. We should rewrite it to
686          * allow us to propagate the error status. */
687         message = _notmuch_message_create_for_message_id (NULL, notmuch,
688                                                           message_id);
689         if (message == NULL) {
690             fprintf (stderr, "Internal error. This shouldn't happen.\n\n");
691             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");
692             exit (1);
693         }
694
695         /* Has a message previously been added with the same ID? */
696         old_filename = notmuch_message_get_filename (message);
697         if (old_filename && strlen (old_filename)) {
698             ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
699             goto DONE;
700         } else {
701             _notmuch_message_set_filename (message, filename);
702             _notmuch_message_add_term (message, "type", "mail");
703         }
704
705         /* Next, find the thread(s) to which this message belongs. */
706         parents = g_ptr_array_new ();
707
708         refs = notmuch_message_file_get_header (message_file, "references");
709         parse_references (parents, refs);
710
711         in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
712         parse_references (parents, in_reply_to);
713
714         for (i = 0; i < parents->len; i++)
715             _notmuch_message_add_term (message, "ref",
716                                        (char *) g_ptr_array_index (parents, i));
717
718         thread_ids = find_thread_ids (notmuch, parents, message_id);
719
720         free (message_id);
721
722         for (i = 0; i < parents->len; i++)
723             g_free (g_ptr_array_index (parents, i));
724         g_ptr_array_free (parents, TRUE);
725
726         if (thread_ids->len) {
727             unsigned int i;
728             GString *thread_id;
729             char *id;
730
731             for (i = 0; i < thread_ids->len; i++) {
732                 id = (char *) thread_ids->pdata[i];
733                 _notmuch_message_add_thread_id (message, id);
734                 if (i == 0)
735                     thread_id = g_string_new (id);
736                 else
737                     g_string_append_printf (thread_id, ",%s", id);
738
739                 free (id);
740             }
741             g_string_free (thread_id, TRUE);
742         } else {
743             _notmuch_message_ensure_thread_id (message);
744         }
745
746         g_ptr_array_free (thread_ids, TRUE);
747
748         date = notmuch_message_file_get_header (message_file, "date");
749         _notmuch_message_set_date (message, date);
750
751         from = notmuch_message_file_get_header (message_file, "from");
752         subject = notmuch_message_file_get_header (message_file, "subject");
753         to = notmuch_message_file_get_header (message_file, "to");
754
755         if (from == NULL &&
756             subject == NULL &&
757             to == NULL)
758         {
759             ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
760             goto DONE;
761         } else {
762             _notmuch_message_sync (message);
763         }
764     } catch (const Xapian::Error &error) {
765         fprintf (stderr, "A Xapian exception occurred: %s.\n",
766                  error.get_msg().c_str());
767         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
768         goto DONE;
769     }
770
771   DONE:
772     if (message)
773         notmuch_message_destroy (message);
774     if (message_file)
775         notmuch_message_file_close (message_file);
776
777     return ret;
778 }