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