16c514591fbe0c36fed80391604fc58096ac697e
[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 Xapian::Document
96 find_document_for_doc_id (notmuch_database_t *notmuch, unsigned doc_id)
97 {
98     return notmuch->xapian_db->get_document (doc_id);
99 }
100
101 static void
102 insert_thread_id (GHashTable *thread_ids, Xapian::Document doc)
103 {
104     string value_string;
105     const char *value, *id, *comma;
106
107     value_string = doc.get_value (NOTMUCH_VALUE_THREAD);
108     value = value_string.c_str();
109     if (strlen (value)) {
110         id = value;
111         while (*id) {
112             comma = strchr (id, ',');
113             if (comma == NULL)
114                 comma = id + strlen (id);
115             g_hash_table_insert (thread_ids,
116                                  strndup (id, comma - id), NULL);
117             id = comma;
118             if (*id)
119                 id++;
120         }
121     }
122 }
123
124 notmuch_message_t *
125 notmuch_database_find_message (notmuch_database_t *notmuch,
126                                const char *message_id)
127 {
128     Xapian::PostingIterator i, end;
129
130     find_doc_ids (notmuch, "msgid", message_id, &i, &end);
131
132     if (i == end)
133         return NULL;
134
135     return _notmuch_message_create (notmuch, notmuch, *i);
136 }
137
138 /* Return one or more thread_ids, (as a GPtrArray of strings), for the
139  * given message based on looking into the database for any messages
140  * referenced in parents, and also for any messages in the database
141  * referencing message_id.
142  *
143  * Caller should free all strings in the array and the array itself,
144  * (g_ptr_array_free) when done. */
145 static GPtrArray *
146 find_thread_ids (notmuch_database_t *notmuch,
147                  GPtrArray *parents,
148                  const char *message_id)
149 {
150     Xapian::PostingIterator child, children_end;
151     Xapian::Document doc;
152     GHashTable *thread_ids;
153     GList *keys, *l;
154     unsigned int i;
155     const char *parent_message_id;
156     GPtrArray *result;
157
158     thread_ids = g_hash_table_new_full (g_str_hash, g_str_equal,
159                                         free, NULL);
160
161     find_doc_ids (notmuch, "ref", message_id, &child, &children_end);
162     for ( ; child != children_end; child++) {
163         doc = find_document_for_doc_id (notmuch, *child);
164         insert_thread_id (thread_ids, doc);
165     }
166
167     for (i = 0; i < parents->len; i++) {
168         notmuch_message_t *parent;
169         notmuch_thread_ids_t *ids;
170
171         parent_message_id = (char *) g_ptr_array_index (parents, i);
172         parent = notmuch_database_find_message (notmuch, parent_message_id);
173         if (parent == NULL)
174             continue;
175
176         for (ids = notmuch_message_get_thread_ids (parent);
177              notmuch_thread_ids_has_more (ids);
178              notmuch_thread_ids_advance (ids))
179         {
180             const char *id;
181
182             id = notmuch_thread_ids_get (ids);
183             g_hash_table_insert (thread_ids, strdup (id), NULL);
184         }
185
186         notmuch_message_destroy (parent);
187     }
188
189     result = g_ptr_array_new ();
190
191     keys = g_hash_table_get_keys (thread_ids);
192     for (l = keys; l; l = l->next) {
193         char *id = (char *) l->data;
194         g_ptr_array_add (result, id);
195     }
196     g_list_free (keys);
197
198     /* We're done with the hash table, but we've taken the pointers to
199      * the allocated strings and put them into our result array, so
200      * tell the hash not to free them on its way out. */
201     g_hash_table_steal_all (thread_ids);
202     g_hash_table_unref (thread_ids);
203
204     return result;
205 }
206
207 /* Advance 'str' past any whitespace or RFC 822 comments. A comment is
208  * a (potentially nested) parenthesized sequence with '\' used to
209  * escape any character (including parentheses).
210  *
211  * If the sequence to be skipped continues to the end of the string,
212  * then 'str' will be left pointing at the final terminating '\0'
213  * character.
214  */
215 static void
216 skip_space_and_comments (const char **str)
217 {
218     const char *s;
219
220     s = *str;
221     while (*s && (isspace (*s) || *s == '(')) {
222         while (*s && isspace (*s))
223             s++;
224         if (*s == '(') {
225             int nesting = 1;
226             s++;
227             while (*s && nesting) {
228                 if (*s == '(')
229                     nesting++;
230                 else if (*s == ')')
231                     nesting--;
232                 else if (*s == '\\')
233                     if (*(s+1))
234                         s++;
235                 s++;
236             }
237         }
238     }
239
240     *str = s;
241 }
242
243 /* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
244  * comments, and the '<' and '>' delimeters.
245  *
246  * If not NULL, then *next will be made to point to the first character
247  * not parsed, (possibly pointing to the final '\0' terminator.
248  *
249  * Returns a newly allocated string which the caller should free()
250  * when done with it.
251  *
252  * Returns NULL if there is any error parsing the message-id. */
253 static char *
254 parse_message_id (const char *message_id, const char **next)
255 {
256     const char *s, *end;
257     char *result;
258
259     if (message_id == NULL)
260         return NULL;
261
262     s = message_id;
263
264     skip_space_and_comments (&s);
265
266     /* Skip any unstructured text as well. */
267     while (*s && *s != '<')
268         s++;
269
270     if (*s == '<') {
271         s++;
272     } else {
273         if (next)
274             *next = s;
275         return NULL;
276     }
277
278     skip_space_and_comments (&s);
279
280     end = s;
281     while (*end && *end != '>')
282         end++;
283     if (next) {
284         if (*end)
285             *next = end + 1;
286         else
287             *next = end;
288     }
289
290     if (end > s && *end == '>')
291         end--;
292     if (end <= s)
293         return NULL;
294
295     result = strndup (s, end - s + 1);
296
297     /* Finally, collapse any whitespace that is within the message-id
298      * itself. */
299     {
300         char *r;
301         int len;
302
303         for (r = result, len = strlen (r); *r; r++, len--)
304             if (*r == ' ' || *r == '\t')
305                 memmove (r, r+1, len);
306     }
307
308     return result;
309 }
310
311 /* Parse a References header value, putting a copy of each referenced
312  * message-id into 'array'. */
313 static void
314 parse_references (GPtrArray *array,
315                   const char *refs)
316 {
317     char *ref;
318
319     if (refs == NULL)
320         return;
321
322     while (*refs) {
323         ref = parse_message_id (refs, &refs);
324
325         if (ref)
326             g_ptr_array_add (array, ref);
327     }
328 }
329
330 char *
331 notmuch_database_default_path (void)
332 {
333     if (getenv ("NOTMUCH_BASE"))
334         return strdup (getenv ("NOTMUCH_BASE"));
335
336     return g_strdup_printf ("%s/mail", getenv ("HOME"));
337 }
338
339 notmuch_database_t *
340 notmuch_database_create (const char *path)
341 {
342     notmuch_database_t *notmuch = NULL;
343     char *notmuch_path = NULL;
344     struct stat st;
345     int err;
346     char *local_path = NULL;
347
348     if (path == NULL)
349         path = local_path = notmuch_database_default_path ();
350
351     err = stat (path, &st);
352     if (err) {
353         fprintf (stderr, "Error: Cannot create database at %s: %s.\n",
354                  path, strerror (errno));
355         goto DONE;
356     }
357
358     if (! S_ISDIR (st.st_mode)) {
359         fprintf (stderr, "Error: Cannot create database at %s: Not a directory.\n",
360                  path);
361         goto DONE;
362     }
363
364     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
365
366     err = mkdir (notmuch_path, 0755);
367
368     if (err) {
369         fprintf (stderr, "Error: Cannot create directory %s: %s.\n",
370                  notmuch_path, strerror (errno));
371         goto DONE;
372     }
373
374     notmuch = notmuch_database_open (path);
375
376   DONE:
377     if (notmuch_path)
378         free (notmuch_path);
379     if (local_path)
380         free (local_path);
381
382     return notmuch;
383 }
384
385 notmuch_database_t *
386 notmuch_database_open (const char *path)
387 {
388     notmuch_database_t *notmuch = NULL;
389     char *notmuch_path = NULL, *xapian_path = NULL;
390     struct stat st;
391     int err;
392     char *local_path = NULL;
393
394     if (path == NULL)
395         path = local_path = notmuch_database_default_path ();
396
397     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
398
399     err = stat (notmuch_path, &st);
400     if (err) {
401         fprintf (stderr, "Error opening database at %s: %s\n",
402                  notmuch_path, strerror (errno));
403         goto DONE;
404     }
405
406     xapian_path = g_strdup_printf ("%s/%s", notmuch_path, "xapian");
407
408     notmuch = talloc (NULL, notmuch_database_t);
409     notmuch->path = talloc_strdup (notmuch, path);
410
411     try {
412         notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
413                                                            Xapian::DB_CREATE_OR_OPEN);
414         notmuch->query_parser = new Xapian::QueryParser;
415         notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
416         notmuch->query_parser->set_database (*notmuch->xapian_db);
417     } catch (const Xapian::Error &error) {
418         fprintf (stderr, "A Xapian exception occurred: %s\n",
419                  error.get_msg().c_str());
420     }
421     
422   DONE:
423     if (local_path)
424         free (local_path);
425     if (notmuch_path)
426         free (notmuch_path);
427     if (xapian_path)
428         free (xapian_path);
429
430     return notmuch;
431 }
432
433 void
434 notmuch_database_close (notmuch_database_t *notmuch)
435 {
436     delete notmuch->query_parser;
437     delete notmuch->xapian_db;
438     talloc_free (notmuch);
439 }
440
441 const char *
442 notmuch_database_get_path (notmuch_database_t *notmuch)
443 {
444     return notmuch->path;
445 }
446
447 notmuch_status_t
448 notmuch_database_add_message (notmuch_database_t *notmuch,
449                               const char *filename)
450 {
451     notmuch_message_file_t *message_file;
452     notmuch_message_t *message;
453     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
454
455     GPtrArray *parents, *thread_ids;
456
457     const char *refs, *in_reply_to, *date, *header;
458     const char *from, *to, *subject, *old_filename;
459     char *message_id;
460
461     unsigned int i;
462
463     message_file = notmuch_message_file_open (filename);
464     if (message_file == NULL) {
465         ret = NOTMUCH_STATUS_FILE_ERROR;
466         goto DONE;
467     }
468
469     notmuch_message_file_restrict_headers (message_file,
470                                            "date",
471                                            "from",
472                                            "in-reply-to",
473                                            "message-id",
474                                            "references",
475                                            "subject",
476                                            "to",
477                                            (char *) NULL);
478
479     try {
480         /* The first order of business is to find/create a message ID. */
481
482         header = notmuch_message_file_get_header (message_file, "message-id");
483         if (header) {
484             message_id = parse_message_id (header, NULL);
485             /* So the header value isn't RFC-compliant, but it's
486              * better than no message-id at all. */
487             if (message_id == NULL)
488                 message_id = xstrdup (header);
489         } else {
490             /* No message-id at all, let's generate one by taking a
491              * hash over the file's contents. */
492             char *sha1 = notmuch_sha1_of_file (filename);
493
494             /* If that failed too, something is really wrong. Give up. */
495             if (sha1 == NULL) {
496                 ret = NOTMUCH_STATUS_FILE_ERROR;
497                 goto DONE;
498             }
499
500             message_id = g_strdup_printf ("notmuch-sha1-%s", sha1);
501             free (sha1);
502         }
503
504         /* Now that we have a message ID, we get a message object,
505          * (which may or may not reference an existing document in the
506          * database). */
507
508         /* Use NULL for owner since we want to free this locally. */
509
510         /* XXX: This call can fail by either out-of-memory or an
511          * "impossible" Xapian exception. We should rewrite it to
512          * allow us to propagate the error status. */
513         message = _notmuch_message_create_for_message_id (NULL, notmuch,
514                                                           message_id);
515         if (message == NULL) {
516             fprintf (stderr, "Internal error. This shouldn't happen.\n\n");
517             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");
518             exit (1);
519         }
520
521         /* Has a message previously been added with the same ID? */
522         old_filename = notmuch_message_get_filename (message);
523         if (old_filename && strlen (old_filename)) {
524             /* XXX: This is too noisy to actually print, and what do we
525              * really expect the user to do? Go manually delete a
526              * redundant message or merge two similar messages?
527              * Instead we should handle this transparently.
528              *
529              * What we likely want to move to is adding both filenames
530              * to the database so that subsequent indexing will pick up
531              * terms from both files.
532              */
533 #if 0
534             fprintf (stderr,
535                      "Note: Attempting to add a message with a duplicate message ID:\n"
536                      "Old: %s\n"   "New: %s\n",
537                      old_filename, filename);
538             fprintf (stderr, "The old filename will be used, but any new terms\n"
539                      "from the new message will added to the database.\n");
540 #endif
541         } else {
542             _notmuch_message_set_filename (message, filename);
543             _notmuch_message_add_term (message, "type", "mail");
544         }
545
546         /* Next, find the thread(s) to which this message belongs. */
547         parents = g_ptr_array_new ();
548
549         refs = notmuch_message_file_get_header (message_file, "references");
550         parse_references (parents, refs);
551
552         in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
553         parse_references (parents, in_reply_to);
554
555         for (i = 0; i < parents->len; i++)
556             _notmuch_message_add_term (message, "ref",
557                                        (char *) g_ptr_array_index (parents, i));
558
559         thread_ids = find_thread_ids (notmuch, parents, message_id);
560
561         free (message_id);
562
563         for (i = 0; i < parents->len; i++)
564             g_free (g_ptr_array_index (parents, i));
565         g_ptr_array_free (parents, TRUE);
566
567         if (thread_ids->len) {
568             unsigned int i;
569             GString *thread_id;
570             char *id;
571
572             for (i = 0; i < thread_ids->len; i++) {
573                 id = (char *) thread_ids->pdata[i];
574                 _notmuch_message_add_thread_id (message, id);
575                 if (i == 0)
576                     thread_id = g_string_new (id);
577                 else
578                     g_string_append_printf (thread_id, ",%s", id);
579
580                 free (id);
581             }
582             g_string_free (thread_id, TRUE);
583         } else {
584             _notmuch_message_ensure_thread_id (message);
585         }
586
587         g_ptr_array_free (thread_ids, TRUE);
588
589         date = notmuch_message_file_get_header (message_file, "date");
590         _notmuch_message_set_date (message, date);
591
592         from = notmuch_message_file_get_header (message_file, "from");
593         subject = notmuch_message_file_get_header (message_file, "subject");
594         to = notmuch_message_file_get_header (message_file, "to");
595
596         if (from == NULL &&
597             subject == NULL &&
598             to == NULL)
599         {
600             ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
601             goto DONE;
602         } else {
603             _notmuch_message_sync (message);
604         }
605     } catch (const Xapian::Error &error) {
606         fprintf (stderr, "A Xapian exception occurred: %s.\n",
607                  error.get_msg().c_str());
608         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
609         goto DONE;
610     }
611
612   DONE:
613     if (message)
614         notmuch_message_destroy (message);
615     if (message_file)
616         notmuch_message_file_close (message_file);
617
618     return ret;
619 }