notmuch: Ignore files that don't look like email messages.
[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 "notmuch-private.h"
22
23 #include <iostream>
24
25 #include <xapian.h>
26
27 #include <glib.h> /* g_strdup_printf, g_free, GHashTable */
28
29 using namespace std;
30
31 struct _notmuch_database {
32     char *path;
33     Xapian::WritableDatabase *xapian_db;
34     Xapian::TermGenerator *term_gen;
35 };
36
37 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
38
39 /* Xapian complains if we provide a term longer than this. */
40 #define NOTMUCH_MAX_TERM 245
41
42 /* These prefix values are specifically chosen to be compatible
43  * with sup, (http://sup.rubyforge.org), written by
44  * William Morgan <wmorgan-sup@masanjin.net>, and released
45  * under the GNU GPL v2.
46  */
47
48 typedef struct {
49     const char *name;
50     const char *prefix;
51 } prefix_t;
52
53 prefix_t NORMAL_PREFIX[] = {
54     { "subject", "S" },
55     { "body", "B" },
56     { "from_name", "FN" },
57     { "to_name", "TN" },
58     { "name", "N" },
59     { "attachment", "A" }
60 };
61
62 prefix_t BOOLEAN_PREFIX[] = {
63     { "type", "K" },
64     { "from_email", "FE" },
65     { "to_email", "TE" },
66     { "email", "E" },
67     { "date", "D" },
68     { "label", "L" },
69     { "source_id", "I" },
70     { "attachment_extension", "O" },
71     { "msgid", "Q" },
72     { "thread", "H" },
73     { "ref", "R" }
74 };
75
76 /* Similarly, these value numbers are also chosen to be sup
77  * compatible. */
78
79 typedef enum {
80     NOTMUCH_VALUE_MESSAGE_ID = 0,
81     NOTMUCH_VALUE_THREAD = 1,
82     NOTMUCH_VALUE_DATE = 2
83 } notmuch_value_t;
84
85 static const char *
86 find_prefix (const char *name)
87 {
88     unsigned int i;
89
90     for (i = 0; i < ARRAY_SIZE (NORMAL_PREFIX); i++)
91         if (strcmp (name, NORMAL_PREFIX[i].name) == 0)
92             return NORMAL_PREFIX[i].prefix;
93
94     for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX); i++)
95         if (strcmp (name, BOOLEAN_PREFIX[i].name) == 0)
96             return BOOLEAN_PREFIX[i].prefix;
97
98     return "";
99 }
100
101 /* "128 bits of thread-id ought to be enough for anybody" */
102 #define NOTMUCH_THREAD_ID_BITS   128
103 #define NOTMUCH_THREAD_ID_DIGITS (NOTMUCH_THREAD_ID_BITS / 4)
104 typedef struct _thread_id {
105     char str[NOTMUCH_THREAD_ID_DIGITS + 1];
106 } thread_id_t;
107
108 static void
109 thread_id_generate (thread_id_t *thread_id)
110 {
111     static int seeded = 0;
112     FILE *dev_random;
113     uint32_t value;
114     char *s;
115     int i;
116
117     if (! seeded) {
118         dev_random = fopen ("/dev/random", "r");
119         if (dev_random == NULL) {
120             srand (time (NULL));
121         } else {
122             fread ((void *) &value, sizeof (value), 1, dev_random);
123             srand (value);
124             fclose (dev_random);
125         }
126         seeded = 1;
127     }
128
129     s = thread_id->str;
130     for (i = 0; i < NOTMUCH_THREAD_ID_DIGITS; i += 8) {
131         value = rand ();
132         sprintf (s, "%08x", value);
133         s += 8;
134     }
135 }
136
137 static void
138 add_term (Xapian::Document doc,
139           const char *prefix_name,
140           const char *value)
141 {
142     const char *prefix;
143     char *term;
144
145     if (value == NULL)
146         return;
147
148     prefix = find_prefix (prefix_name);
149
150     term = g_strdup_printf ("%s%s", prefix, value);
151
152     if (strlen (term) <= NOTMUCH_MAX_TERM)
153         doc.add_term (term);
154
155     g_free (term);
156 }
157
158 static void
159 find_messages_by_term (Xapian::Database *db,
160                        const char *prefix_name,
161                        const char *value,
162                        Xapian::PostingIterator *begin,
163                        Xapian::PostingIterator *end)
164 {
165     Xapian::PostingIterator i;
166     char *term;
167
168     term = g_strdup_printf ("%s%s", find_prefix (prefix_name), value);
169
170     *begin = db->postlist_begin (term);
171
172     if (end)
173         *end = db->postlist_end (term);
174
175     free (term);
176 }
177
178 Xapian::Document
179 find_message_by_docid (Xapian::Database *db, Xapian::docid docid)
180 {
181     return db->get_document (docid);
182 }
183
184 Xapian::Document
185 find_message_by_message_id (Xapian::Database *db, const char *message_id)
186 {
187     Xapian::PostingIterator i, end;
188
189     find_messages_by_term (db, "msgid", message_id, &i, &end);
190
191     if (i != end)
192         return find_message_by_docid (db, *i);
193     else
194         return Xapian::Document ();
195 }
196
197 static void
198 insert_thread_id (GHashTable *thread_ids, Xapian::Document doc)
199 {
200     string value_string;
201     const char *value, *id, *comma;
202
203     value_string = doc.get_value (NOTMUCH_VALUE_THREAD);
204     value = value_string.c_str();
205     if (strlen (value)) {
206         id = value;
207         while (*id) {
208             comma = strchr (id, ',');
209             if (comma == NULL)
210                 comma = id + strlen (id);
211             g_hash_table_insert (thread_ids,
212                                  strndup (id, comma - id), NULL);
213             id = comma;
214             if (*id)
215                 id++;
216         }
217     }
218 }
219
220 /* Return one or more thread_ids, (as a GPtrArray of strings), for the
221  * given message based on looking into the database for any messages
222  * referenced in parents, and also for any messages in the database
223  * referencing message_id.
224  *
225  * Caller should free all strings in the array and the array itself,
226  * (g_ptr_array_free) when done. */
227 static GPtrArray *
228 find_thread_ids (Xapian::Database *db,
229                  GPtrArray *parents,
230                  const char *message_id)
231 {
232     Xapian::PostingIterator child, children_end;
233     Xapian::Document doc;
234     GHashTable *thread_ids;
235     GList *keys, *l;
236     unsigned int i;
237     const char *parent_message_id;
238     GPtrArray *result;
239
240     thread_ids = g_hash_table_new_full (g_str_hash, g_str_equal,
241                                         free, NULL);
242
243     find_messages_by_term (db, "ref", message_id, &child, &children_end);
244     for ( ; child != children_end; child++) {
245         doc = find_message_by_docid (db, *child);
246         insert_thread_id (thread_ids, doc);
247     }
248
249     for (i = 0; i < parents->len; i++) {
250         parent_message_id = (char *) g_ptr_array_index (parents, i);
251         doc = find_message_by_message_id (db, parent_message_id);
252         insert_thread_id (thread_ids, doc);
253     }
254
255     result = g_ptr_array_new ();
256
257     keys = g_hash_table_get_keys (thread_ids);
258     for (l = keys; l; l = l->next) {
259         char *id = (char *) l->data;
260         g_ptr_array_add (result, id);
261     }
262     g_list_free (keys);
263
264     /* We're done with the hash table, but we've taken the pointers to
265      * the allocated strings and put them into our result array, so
266      * tell the hash not to free them on its way out. */
267     g_hash_table_steal_all (thread_ids);
268     g_hash_table_unref (thread_ids);
269
270     return result;
271 }
272
273 /* Advance 'str' past any whitespace or RFC 822 comments. A comment is
274  * a (potentially nested) parenthesized sequence with '\' used to
275  * escape any character (including parentheses).
276  *
277  * If the sequence to be skipped continues to the end of the string,
278  * then 'str' will be left pointing at the final terminating '\0'
279  * character.
280  */
281 static void
282 skip_space_and_comments (const char **str)
283 {
284     const char *s;
285
286     s = *str;
287     while (*s && (isspace (*s) || *s == '(')) {
288         while (*s && isspace (*s))
289             s++;
290         if (*s == '(') {
291             int nesting = 1;
292             s++;
293             while (*s && nesting) {
294                 if (*s == '(')
295                     nesting++;
296                 else if (*s == ')')
297                     nesting--;
298                 else if (*s == '\\')
299                     if (*(s+1))
300                         s++;
301                 s++;
302             }
303         }
304     }
305
306     *str = s;
307 }
308
309 /* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
310  * comments, and the '<' and '>' delimeters.
311  *
312  * If not NULL, then *next will be made to point to the first character
313  * not parsed, (possibly pointing to the final '\0' terminator.
314  *
315  * Returns a newly allocated string which the caller should free()
316  * when done with it.
317  *
318  * Returns NULL if there is any error parsing the message-id. */
319 static char *
320 parse_message_id (const char *message_id, const char **next)
321 {
322     const char *s, *end;
323
324     if (message_id == NULL)
325         return NULL;
326
327     s = message_id;
328
329     skip_space_and_comments (&s);
330
331     /* Skip any unstructured text as well. */
332     while (*s && *s != '<')
333         s++;
334
335     if (*s == '<') {
336         s++;
337     } else {
338         if (next)
339             *next = s;
340         return NULL;
341     }
342
343     skip_space_and_comments (&s);
344
345     end = s;
346     while (*end && *end != '>')
347         end++;
348     if (next) {
349         if (*end)
350             *next = end + 1;
351         else
352             *next = end;
353     }
354
355     if (end > s && *end == '>')
356         end--;
357     if (end > s)
358         return strndup (s, end - s + 1);
359     else
360         return NULL;
361 }
362
363 /* Parse a References header value, putting a copy of each referenced
364  * message-id into 'array'. */
365 static void
366 parse_references (GPtrArray *array,
367                   const char *refs)
368 {
369     char *ref;
370
371     if (refs == NULL)
372         return;
373
374     while (*refs) {
375         ref = parse_message_id (refs, &refs);
376
377         if (ref)
378             g_ptr_array_add (array, ref);
379     }
380 }
381
382 notmuch_database_t *
383 notmuch_database_create (const char *path)
384 {
385     char *notmuch_path;
386     struct stat st;
387     int err;
388
389     err = stat (path, &st);
390     if (err) {
391         fprintf (stderr, "Error: Cannot create database at %s: %s.\n",
392                  path, strerror (errno));
393         return NULL;
394     }
395
396     if (! S_ISDIR (st.st_mode)) {
397         fprintf (stderr, "Error: Cannot create database at %s: Not a directory.\n",
398                  path);
399         return NULL;
400     }
401
402     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
403
404     err = mkdir (notmuch_path, 0755);
405
406     if (err) {
407         fprintf (stderr, "Error: Cannot create directory %s: %s.\n",
408                  notmuch_path, strerror (errno));
409         free (notmuch_path);
410         return NULL;
411     }
412
413     free (notmuch_path);
414
415     return notmuch_database_open (path);
416 }
417
418 notmuch_database_t *
419 notmuch_database_open (const char *path)
420 {
421     notmuch_database_t *notmuch;
422     char *notmuch_path, *xapian_path;
423     struct stat st;
424     int err;
425
426     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
427
428     err = stat (notmuch_path, &st);
429     if (err) {
430         fprintf (stderr, "Error: Cannot stat %s: %s\n",
431                  notmuch_path, strerror (err));
432         free (notmuch_path);
433         return NULL;
434     }
435
436     xapian_path = g_strdup_printf ("%s/%s", notmuch_path, "xapian");
437     free (notmuch_path);
438
439     /* C++ is so nasty in requiring these casts. I'm almost tempted to
440      * write a C wrapper for Xapian... */
441     notmuch = (notmuch_database_t *) xmalloc (sizeof (notmuch_database_t));
442     notmuch->path = xstrdup (path);
443
444     try {
445         notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
446                                                            Xapian::DB_CREATE_OR_OPEN);
447     } catch (const Xapian::Error &error) {
448         fprintf (stderr, "A Xapian exception occurred: %s\n",
449                  error.get_msg().c_str());
450     }
451     
452     free (xapian_path);
453
454     return notmuch;
455 }
456
457 void
458 notmuch_database_close (notmuch_database_t *notmuch)
459 {
460     delete notmuch->xapian_db;
461     free (notmuch->path);
462     free (notmuch);
463 }
464
465 const char *
466 notmuch_database_get_path (notmuch_database_t *notmuch)
467 {
468     return notmuch->path;
469 }
470
471 notmuch_status_t
472 notmuch_database_add_message (notmuch_database_t *notmuch,
473                               const char *filename)
474 {
475     Xapian::WritableDatabase *db = notmuch->xapian_db;
476     Xapian::Document doc;
477     notmuch_message_t *message;
478
479     GPtrArray *parents, *thread_ids;
480
481     const char *refs, *in_reply_to, *date, *header;
482     const char *from, *to, *subject;
483     char *message_id;
484
485     time_t time_value;
486     unsigned int i;
487
488     message = notmuch_message_open (filename);
489
490     notmuch_message_restrict_headers (message,
491                                       "date",
492                                       "from",
493                                       "in-reply-to",
494                                       "message-id",
495                                       "references",
496                                       "subject",
497                                       (char *) NULL);
498
499     try {
500         doc = Xapian::Document ();
501
502         doc.set_data (filename);
503
504         parents = g_ptr_array_new ();
505
506         refs = notmuch_message_get_header (message, "references");
507         parse_references (parents, refs);
508
509         in_reply_to = notmuch_message_get_header (message, "in-reply-to");
510         parse_references (parents, in_reply_to);
511
512         for (i = 0; i < parents->len; i++)
513             add_term (doc, "ref", (char *) g_ptr_array_index (parents, i));
514
515         header = notmuch_message_get_header (message, "message-id");
516         if (header) {
517             message_id = parse_message_id (header, NULL);
518             /* So the header value isn't RFC-compliant, but it's
519              * better than no message-id at all. */
520             if (message_id == NULL)
521                 message_id = xstrdup (header);
522         } else {
523             /* XXX: Should generate a message_id here, (such as a SHA1
524              * sum of the message itself) */
525             message_id = NULL;
526         }
527
528         thread_ids = find_thread_ids (db, parents, message_id);
529
530         for (i = 0; i < parents->len; i++)
531             g_free (g_ptr_array_index (parents, i));
532         g_ptr_array_free (parents, TRUE);
533         if (message_id) {
534             add_term (doc, "msgid", message_id);
535             doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id);
536         }
537
538         if (thread_ids->len) {
539             unsigned int i;
540             GString *thread_id;
541             char *id;
542
543             for (i = 0; i < thread_ids->len; i++) {
544                 id = (char *) thread_ids->pdata[i];
545                 add_term (doc, "thread", id);
546                 if (i == 0)
547                     thread_id = g_string_new (id);
548                 else
549                     g_string_append_printf (thread_id, ",%s", id);
550
551                 free (id);
552             }
553             g_ptr_array_free (thread_ids, TRUE);
554             doc.add_value (NOTMUCH_VALUE_THREAD, thread_id->str);
555             g_string_free (thread_id, TRUE);
556         } else if (message_id) {
557             /* If not part of any existing thread, generate a new thread_id. */
558             thread_id_t thread_id;
559
560             thread_id_generate (&thread_id);
561             add_term (doc, "thread", thread_id.str);
562             doc.add_value (NOTMUCH_VALUE_THREAD, thread_id.str);
563         }
564
565         free (message_id);
566
567         date = notmuch_message_get_header (message, "date");
568         time_value = notmuch_parse_date (date, NULL);
569
570         doc.add_value (NOTMUCH_VALUE_DATE,
571                        Xapian::sortable_serialise (time_value));
572
573         from = notmuch_message_get_header (message, "from");
574         subject = notmuch_message_get_header (message, "subject");
575         to = notmuch_message_get_header (message, "to");
576
577         if (from == NULL &&
578             subject == NULL &&
579             to == NULL)
580         {
581             notmuch_message_close (message);
582             return NOTMUCH_STATUS_FILE_NOT_EMAIL;
583         } else {
584             db->add_document (doc);
585         }
586     } catch (const Xapian::Error &error) {
587         fprintf (stderr, "A Xapian exception occurred: %s.\n",
588                  error.get_msg().c_str());
589         return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
590     }
591
592     notmuch_message_close (message);
593
594     return NOTMUCH_STATUS_SUCCESS;
595 }