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