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