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