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