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