database: Add private find_unique_doc_id and find_unique_document functions
[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_status_t
488 notmuch_database_add_message (notmuch_database_t *notmuch,
489                               const char *filename)
490 {
491     notmuch_message_file_t *message_file;
492     notmuch_message_t *message;
493     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
494
495     GPtrArray *parents, *thread_ids;
496
497     const char *refs, *in_reply_to, *date, *header;
498     const char *from, *to, *subject, *old_filename;
499     char *message_id;
500
501     unsigned int i;
502
503     message_file = notmuch_message_file_open (filename);
504     if (message_file == NULL) {
505         ret = NOTMUCH_STATUS_FILE_ERROR;
506         goto DONE;
507     }
508
509     notmuch_message_file_restrict_headers (message_file,
510                                            "date",
511                                            "from",
512                                            "in-reply-to",
513                                            "message-id",
514                                            "references",
515                                            "subject",
516                                            "to",
517                                            (char *) NULL);
518
519     try {
520         /* The first order of business is to find/create a message ID. */
521
522         header = notmuch_message_file_get_header (message_file, "message-id");
523         if (header) {
524             message_id = parse_message_id (header, NULL);
525             /* So the header value isn't RFC-compliant, but it's
526              * better than no message-id at all. */
527             if (message_id == NULL)
528                 message_id = xstrdup (header);
529         } else {
530             /* No message-id at all, let's generate one by taking a
531              * hash over the file's contents. */
532             char *sha1 = notmuch_sha1_of_file (filename);
533
534             /* If that failed too, something is really wrong. Give up. */
535             if (sha1 == NULL) {
536                 ret = NOTMUCH_STATUS_FILE_ERROR;
537                 goto DONE;
538             }
539
540             message_id = g_strdup_printf ("notmuch-sha1-%s", sha1);
541             free (sha1);
542         }
543
544         /* Now that we have a message ID, we get a message object,
545          * (which may or may not reference an existing document in the
546          * database). */
547
548         /* Use NULL for owner since we want to free this locally. */
549
550         /* XXX: This call can fail by either out-of-memory or an
551          * "impossible" Xapian exception. We should rewrite it to
552          * allow us to propagate the error status. */
553         message = _notmuch_message_create_for_message_id (NULL, notmuch,
554                                                           message_id);
555         if (message == NULL) {
556             fprintf (stderr, "Internal error. This shouldn't happen.\n\n");
557             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");
558             exit (1);
559         }
560
561         /* Has a message previously been added with the same ID? */
562         old_filename = notmuch_message_get_filename (message);
563         if (old_filename && strlen (old_filename)) {
564             /* XXX: This is too noisy to actually print, and what do we
565              * really expect the user to do? Go manually delete a
566              * redundant message or merge two similar messages?
567              * Instead we should handle this transparently.
568              *
569              * What we likely want to move to is adding both filenames
570              * to the database so that subsequent indexing will pick up
571              * terms from both files.
572              */
573 #if 0
574             fprintf (stderr,
575                      "Note: Attempting to add a message with a duplicate message ID:\n"
576                      "Old: %s\n"   "New: %s\n",
577                      old_filename, filename);
578             fprintf (stderr, "The old filename will be used, but any new terms\n"
579                      "from the new message will added to the database.\n");
580 #endif
581         } else {
582             _notmuch_message_set_filename (message, filename);
583             _notmuch_message_add_term (message, "type", "mail");
584         }
585
586         /* Next, find the thread(s) to which this message belongs. */
587         parents = g_ptr_array_new ();
588
589         refs = notmuch_message_file_get_header (message_file, "references");
590         parse_references (parents, refs);
591
592         in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
593         parse_references (parents, in_reply_to);
594
595         for (i = 0; i < parents->len; i++)
596             _notmuch_message_add_term (message, "ref",
597                                        (char *) g_ptr_array_index (parents, i));
598
599         thread_ids = find_thread_ids (notmuch, parents, message_id);
600
601         free (message_id);
602
603         for (i = 0; i < parents->len; i++)
604             g_free (g_ptr_array_index (parents, i));
605         g_ptr_array_free (parents, TRUE);
606
607         if (thread_ids->len) {
608             unsigned int i;
609             GString *thread_id;
610             char *id;
611
612             for (i = 0; i < thread_ids->len; i++) {
613                 id = (char *) thread_ids->pdata[i];
614                 _notmuch_message_add_thread_id (message, id);
615                 if (i == 0)
616                     thread_id = g_string_new (id);
617                 else
618                     g_string_append_printf (thread_id, ",%s", id);
619
620                 free (id);
621             }
622             g_string_free (thread_id, TRUE);
623         } else {
624             _notmuch_message_ensure_thread_id (message);
625         }
626
627         g_ptr_array_free (thread_ids, TRUE);
628
629         date = notmuch_message_file_get_header (message_file, "date");
630         _notmuch_message_set_date (message, date);
631
632         from = notmuch_message_file_get_header (message_file, "from");
633         subject = notmuch_message_file_get_header (message_file, "subject");
634         to = notmuch_message_file_get_header (message_file, "to");
635
636         if (from == NULL &&
637             subject == NULL &&
638             to == NULL)
639         {
640             ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
641             goto DONE;
642         } else {
643             _notmuch_message_sync (message);
644         }
645     } catch (const Xapian::Error &error) {
646         fprintf (stderr, "A Xapian exception occurred: %s.\n",
647                  error.get_msg().c_str());
648         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
649         goto DONE;
650     }
651
652   DONE:
653     if (message)
654         notmuch_message_destroy (message);
655     if (message_file)
656         notmuch_message_file_close (message_file);
657
658     return ret;
659 }