Fix timestamp generation to avoid overflowing the term limit
[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 #define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
32
33 /* These prefix values are specifically chosen to be compatible
34  * with sup, (http://sup.rubyforge.org), written by
35  * William Morgan <wmorgan-sup@masanjin.net>, and released
36  * under the GNU GPL v2.
37  */
38
39 typedef struct {
40     const char *name;
41     const char *prefix;
42 } prefix_t;
43
44 prefix_t BOOLEAN_PREFIX[] = {
45     { "type", "K" },
46     { "tag", "L" },
47     { "msgid", "Q" },
48     { "thread", "H" },
49     { "ref", "R" },
50     { "timestamp", "KTS" },
51 };
52
53 const char *
54 _find_prefix (const char *name)
55 {
56     unsigned int i;
57
58     for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX); i++)
59         if (strcmp (name, BOOLEAN_PREFIX[i].name) == 0)
60             return BOOLEAN_PREFIX[i].prefix;
61
62     fprintf (stderr, "Internal error: No prefix exists for '%s'\n", name);
63     exit (1);
64
65     return "";
66 }
67
68 const char *
69 notmuch_status_to_string (notmuch_status_t status)
70 {
71     switch (status) {
72     case NOTMUCH_STATUS_SUCCESS:
73         return "No error occurred";
74     case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
75         return "A Xapian exception occurred";
76     case NOTMUCH_STATUS_FILE_ERROR:
77         return "Something went wrong trying to read or write a file";
78     case NOTMUCH_STATUS_FILE_NOT_EMAIL:
79         return "File is not an email";
80     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
81         return "Message ID is identical to a message in database";
82     case NOTMUCH_STATUS_NULL_POINTER:
83         return "Erroneous NULL pointer";
84     case NOTMUCH_STATUS_TAG_TOO_LONG:
85         return "Tag value is too long (exceeds NOTMUCH_TAG_MAX)";
86     default:
87     case NOTMUCH_STATUS_LAST_STATUS:
88         return "Unknown error status value";
89     }
90 }
91
92 /* XXX: We should drop this function and convert all callers to call
93  * _notmuch_message_add_term instead. */
94 static void
95 add_term (Xapian::Document doc,
96           const char *prefix_name,
97           const char *value)
98 {
99     const char *prefix;
100     char *term;
101
102     if (value == NULL)
103         return;
104
105     prefix = _find_prefix (prefix_name);
106
107     term = g_strdup_printf ("%s%s", prefix, value);
108
109     if (strlen (term) <= NOTMUCH_TERM_MAX)
110         doc.add_term (term);
111
112     g_free (term);
113 }
114
115 static void
116 find_doc_ids (notmuch_database_t *notmuch,
117               const char *prefix_name,
118               const char *value,
119               Xapian::PostingIterator *begin,
120               Xapian::PostingIterator *end)
121 {
122     Xapian::PostingIterator i;
123     char *term;
124
125     term = g_strdup_printf ("%s%s", _find_prefix (prefix_name), value);
126
127     *begin = notmuch->xapian_db->postlist_begin (term);
128
129     *end = notmuch->xapian_db->postlist_end (term);
130
131     free (term);
132 }
133
134 static notmuch_private_status_t
135 find_unique_doc_id (notmuch_database_t *notmuch,
136                     const char *prefix_name,
137                     const char *value,
138                     unsigned int *doc_id)
139 {
140     Xapian::PostingIterator i, end;
141
142     find_doc_ids (notmuch, prefix_name, value, &i, &end);
143
144     if (i == end) {
145         *doc_id = 0;
146         return NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
147     } else {
148         *doc_id = *i;
149         return NOTMUCH_PRIVATE_STATUS_SUCCESS;
150     }
151 }
152
153 static Xapian::Document
154 find_document_for_doc_id (notmuch_database_t *notmuch, unsigned doc_id)
155 {
156     return notmuch->xapian_db->get_document (doc_id);
157 }
158
159 static notmuch_private_status_t
160 find_unique_document (notmuch_database_t *notmuch,
161                       const char *prefix_name,
162                       const char *value,
163                       Xapian::Document *document,
164                       unsigned int *doc_id)
165 {
166     notmuch_private_status_t status;
167
168     status = find_unique_doc_id (notmuch, prefix_name, value, doc_id);
169
170     if (status) {
171         *document = Xapian::Document ();
172         return status;
173     }
174
175     *document = find_document_for_doc_id (notmuch, *doc_id);
176     return NOTMUCH_PRIVATE_STATUS_SUCCESS;
177 }
178
179 static void
180 insert_thread_id (GHashTable *thread_ids, Xapian::Document doc)
181 {
182     string value_string;
183     const char *value, *id, *comma;
184
185     value_string = doc.get_value (NOTMUCH_VALUE_THREAD);
186     value = value_string.c_str();
187     if (strlen (value)) {
188         id = value;
189         while (*id) {
190             comma = strchr (id, ',');
191             if (comma == NULL)
192                 comma = id + strlen (id);
193             g_hash_table_insert (thread_ids,
194                                  strndup (id, comma - id), NULL);
195             id = comma;
196             if (*id)
197                 id++;
198         }
199     }
200 }
201
202 notmuch_message_t *
203 notmuch_database_find_message (notmuch_database_t *notmuch,
204                                const char *message_id)
205 {
206     notmuch_private_status_t status;
207     unsigned int doc_id;
208
209     status = find_unique_doc_id (notmuch, "msgid", message_id, &doc_id);
210
211     if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
212         return NULL;
213
214     return _notmuch_message_create (notmuch, notmuch, doc_id);
215 }
216
217 /* Return one or more thread_ids, (as a GPtrArray of strings), for the
218  * given message based on looking into the database for any messages
219  * referenced in parents, and also for any messages in the database
220  * referencing message_id.
221  *
222  * Caller should free all strings in the array and the array itself,
223  * (g_ptr_array_free) when done. */
224 static GPtrArray *
225 find_thread_ids (notmuch_database_t *notmuch,
226                  GPtrArray *parents,
227                  const char *message_id)
228 {
229     Xapian::PostingIterator child, children_end;
230     Xapian::Document doc;
231     GHashTable *thread_ids;
232     GList *keys, *l;
233     unsigned int i;
234     const char *parent_message_id;
235     GPtrArray *result;
236
237     thread_ids = g_hash_table_new_full (g_str_hash, g_str_equal,
238                                         free, NULL);
239
240     find_doc_ids (notmuch, "ref", message_id, &child, &children_end);
241     for ( ; child != children_end; child++) {
242         doc = find_document_for_doc_id (notmuch, *child);
243         insert_thread_id (thread_ids, doc);
244     }
245
246     for (i = 0; i < parents->len; i++) {
247         notmuch_message_t *parent;
248         notmuch_thread_ids_t *ids;
249
250         parent_message_id = (char *) g_ptr_array_index (parents, i);
251         parent = notmuch_database_find_message (notmuch, parent_message_id);
252         if (parent == NULL)
253             continue;
254
255         for (ids = notmuch_message_get_thread_ids (parent);
256              notmuch_thread_ids_has_more (ids);
257              notmuch_thread_ids_advance (ids))
258         {
259             const char *id;
260
261             id = notmuch_thread_ids_get (ids);
262             g_hash_table_insert (thread_ids, strdup (id), NULL);
263         }
264
265         notmuch_message_destroy (parent);
266     }
267
268     result = g_ptr_array_new ();
269
270     keys = g_hash_table_get_keys (thread_ids);
271     for (l = keys; l; l = l->next) {
272         char *id = (char *) l->data;
273         g_ptr_array_add (result, id);
274     }
275     g_list_free (keys);
276
277     /* We're done with the hash table, but we've taken the pointers to
278      * the allocated strings and put them into our result array, so
279      * tell the hash not to free them on its way out. */
280     g_hash_table_steal_all (thread_ids);
281     g_hash_table_unref (thread_ids);
282
283     return result;
284 }
285
286 /* Advance 'str' past any whitespace or RFC 822 comments. A comment is
287  * a (potentially nested) parenthesized sequence with '\' used to
288  * escape any character (including parentheses).
289  *
290  * If the sequence to be skipped continues to the end of the string,
291  * then 'str' will be left pointing at the final terminating '\0'
292  * character.
293  */
294 static void
295 skip_space_and_comments (const char **str)
296 {
297     const char *s;
298
299     s = *str;
300     while (*s && (isspace (*s) || *s == '(')) {
301         while (*s && isspace (*s))
302             s++;
303         if (*s == '(') {
304             int nesting = 1;
305             s++;
306             while (*s && nesting) {
307                 if (*s == '(')
308                     nesting++;
309                 else if (*s == ')')
310                     nesting--;
311                 else if (*s == '\\')
312                     if (*(s+1))
313                         s++;
314                 s++;
315             }
316         }
317     }
318
319     *str = s;
320 }
321
322 /* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
323  * comments, and the '<' and '>' delimeters.
324  *
325  * If not NULL, then *next will be made to point to the first character
326  * not parsed, (possibly pointing to the final '\0' terminator.
327  *
328  * Returns a newly allocated string which the caller should free()
329  * when done with it.
330  *
331  * Returns NULL if there is any error parsing the message-id. */
332 static char *
333 parse_message_id (const char *message_id, const char **next)
334 {
335     const char *s, *end;
336     char *result;
337
338     if (message_id == NULL)
339         return NULL;
340
341     s = message_id;
342
343     skip_space_and_comments (&s);
344
345     /* Skip any unstructured text as well. */
346     while (*s && *s != '<')
347         s++;
348
349     if (*s == '<') {
350         s++;
351     } else {
352         if (next)
353             *next = s;
354         return NULL;
355     }
356
357     skip_space_and_comments (&s);
358
359     end = s;
360     while (*end && *end != '>')
361         end++;
362     if (next) {
363         if (*end)
364             *next = end + 1;
365         else
366             *next = end;
367     }
368
369     if (end > s && *end == '>')
370         end--;
371     if (end <= s)
372         return NULL;
373
374     result = strndup (s, end - s + 1);
375
376     /* Finally, collapse any whitespace that is within the message-id
377      * itself. */
378     {
379         char *r;
380         int len;
381
382         for (r = result, len = strlen (r); *r; r++, len--)
383             if (*r == ' ' || *r == '\t')
384                 memmove (r, r+1, len);
385     }
386
387     return result;
388 }
389
390 /* Parse a References header value, putting a copy of each referenced
391  * message-id into 'array'. */
392 static void
393 parse_references (GPtrArray *array,
394                   const char *refs)
395 {
396     char *ref;
397
398     if (refs == NULL)
399         return;
400
401     while (*refs) {
402         ref = parse_message_id (refs, &refs);
403
404         if (ref)
405             g_ptr_array_add (array, ref);
406     }
407 }
408
409 char *
410 notmuch_database_default_path (void)
411 {
412     if (getenv ("NOTMUCH_BASE"))
413         return strdup (getenv ("NOTMUCH_BASE"));
414
415     return g_strdup_printf ("%s/mail", getenv ("HOME"));
416 }
417
418 notmuch_database_t *
419 notmuch_database_create (const char *path)
420 {
421     notmuch_database_t *notmuch = NULL;
422     char *notmuch_path = NULL;
423     struct stat st;
424     int err;
425     char *local_path = NULL;
426
427     if (path == NULL)
428         path = local_path = notmuch_database_default_path ();
429
430     err = stat (path, &st);
431     if (err) {
432         fprintf (stderr, "Error: Cannot create database at %s: %s.\n",
433                  path, strerror (errno));
434         goto DONE;
435     }
436
437     if (! S_ISDIR (st.st_mode)) {
438         fprintf (stderr, "Error: Cannot create database at %s: Not a directory.\n",
439                  path);
440         goto DONE;
441     }
442
443     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
444
445     err = mkdir (notmuch_path, 0755);
446
447     if (err) {
448         fprintf (stderr, "Error: Cannot create directory %s: %s.\n",
449                  notmuch_path, strerror (errno));
450         goto DONE;
451     }
452
453     notmuch = notmuch_database_open (path);
454
455   DONE:
456     if (notmuch_path)
457         free (notmuch_path);
458     if (local_path)
459         free (local_path);
460
461     return notmuch;
462 }
463
464 notmuch_database_t *
465 notmuch_database_open (const char *path)
466 {
467     notmuch_database_t *notmuch = NULL;
468     char *notmuch_path = NULL, *xapian_path = NULL;
469     struct stat st;
470     int err;
471     char *local_path = NULL;
472
473     if (path == NULL)
474         path = local_path = notmuch_database_default_path ();
475
476     notmuch_path = g_strdup_printf ("%s/%s", path, ".notmuch");
477
478     err = stat (notmuch_path, &st);
479     if (err) {
480         fprintf (stderr, "Error opening database at %s: %s\n",
481                  notmuch_path, strerror (errno));
482         goto DONE;
483     }
484
485     xapian_path = g_strdup_printf ("%s/%s", notmuch_path, "xapian");
486
487     notmuch = talloc (NULL, notmuch_database_t);
488     notmuch->path = talloc_strdup (notmuch, path);
489
490     try {
491         notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
492                                                            Xapian::DB_CREATE_OR_OPEN);
493         notmuch->query_parser = new Xapian::QueryParser;
494         notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
495         notmuch->query_parser->set_database (*notmuch->xapian_db);
496     } catch (const Xapian::Error &error) {
497         fprintf (stderr, "A Xapian exception occurred: %s\n",
498                  error.get_msg().c_str());
499     }
500     
501   DONE:
502     if (local_path)
503         free (local_path);
504     if (notmuch_path)
505         free (notmuch_path);
506     if (xapian_path)
507         free (xapian_path);
508
509     return notmuch;
510 }
511
512 void
513 notmuch_database_close (notmuch_database_t *notmuch)
514 {
515     delete notmuch->query_parser;
516     delete notmuch->xapian_db;
517     talloc_free (notmuch);
518 }
519
520 const char *
521 notmuch_database_get_path (notmuch_database_t *notmuch)
522 {
523     return notmuch->path;
524 }
525
526 notmuch_private_status_t
527 find_timestamp_document (notmuch_database_t *notmuch, const char *db_key,
528                          Xapian::Document *doc, unsigned int *doc_id)
529 {
530     return find_unique_document (notmuch, "timestamp", db_key, doc, doc_id);
531 }
532
533 /* We allow the user to use arbitrarily long keys for timestamps,
534  * (they're for filesystem paths after all, which have no limit we
535  * know about). But we have a term-length limit. So if we exceed that,
536  * we'll use the SHA-1 of the user's key as the actual key for
537  * constructing a database term.
538  *
539  * Caution: This function returns a newly allocated string which the
540  * caller should free() when finished.
541  */
542 static char *
543 timestamp_db_key (const char *key)
544 {
545     int term_len = strlen (_find_prefix ("timestamp")) + strlen (key);
546
547     if (term_len > NOTMUCH_TERM_MAX)
548         return notmuch_sha1_of_string (key);
549     else
550         return strdup (key);
551 }
552
553 notmuch_status_t
554 notmuch_database_set_timestamp (notmuch_database_t *notmuch,
555                                 const char *key, time_t timestamp)
556 {
557     Xapian::Document doc;
558     unsigned int doc_id;
559     notmuch_private_status_t status;
560     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
561     char *db_key = NULL;
562
563     db_key = timestamp_db_key (key);
564
565     try {
566         status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
567
568         doc.add_value (0, Xapian::sortable_serialise (timestamp));
569
570         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
571             char *term = talloc_asprintf (NULL, "%s%s",
572                                           _find_prefix ("timestamp"), db_key);
573             doc.add_term (term);
574             talloc_free (term);
575
576             notmuch->xapian_db->add_document (doc);
577         } else {
578             notmuch->xapian_db->replace_document (doc_id, doc);
579         }
580
581     } catch (Xapian::Error &error) {
582         fprintf (stderr, "A Xapian exception occurred: %s.\n",
583                  error.get_msg().c_str());
584         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
585     }
586
587     if (db_key)
588         free (db_key);
589
590     return ret;
591 }
592
593 time_t
594 notmuch_database_get_timestamp (notmuch_database_t *notmuch, const char *key)
595 {
596     Xapian::Document doc;
597     unsigned int doc_id;
598     notmuch_private_status_t status;
599     char *db_key = NULL;
600     time_t ret = 0;
601
602     db_key = timestamp_db_key (key);
603
604     try {
605         status = find_timestamp_document (notmuch, db_key, &doc, &doc_id);
606
607         if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
608             goto DONE;
609
610         ret =  Xapian::sortable_unserialise (doc.get_value (0));
611     } catch (Xapian::Error &error) {
612         goto DONE;
613     }
614
615   DONE:
616     if (db_key)
617         free (db_key);
618
619     return ret;
620 }
621
622 notmuch_status_t
623 notmuch_database_add_message (notmuch_database_t *notmuch,
624                               const char *filename)
625 {
626     notmuch_message_file_t *message_file;
627     notmuch_message_t *message;
628     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
629
630     GPtrArray *parents, *thread_ids;
631
632     const char *refs, *in_reply_to, *date, *header;
633     const char *from, *to, *subject, *old_filename;
634     char *message_id;
635
636     unsigned int i;
637
638     message_file = notmuch_message_file_open (filename);
639     if (message_file == NULL) {
640         ret = NOTMUCH_STATUS_FILE_ERROR;
641         goto DONE;
642     }
643
644     notmuch_message_file_restrict_headers (message_file,
645                                            "date",
646                                            "from",
647                                            "in-reply-to",
648                                            "message-id",
649                                            "references",
650                                            "subject",
651                                            "to",
652                                            (char *) NULL);
653
654     try {
655         /* The first order of business is to find/create a message ID. */
656
657         header = notmuch_message_file_get_header (message_file, "message-id");
658         if (header) {
659             message_id = parse_message_id (header, NULL);
660             /* So the header value isn't RFC-compliant, but it's
661              * better than no message-id at all. */
662             if (message_id == NULL)
663                 message_id = xstrdup (header);
664         } else {
665             /* No message-id at all, let's generate one by taking a
666              * hash over the file's contents. */
667             char *sha1 = notmuch_sha1_of_file (filename);
668
669             /* If that failed too, something is really wrong. Give up. */
670             if (sha1 == NULL) {
671                 ret = NOTMUCH_STATUS_FILE_ERROR;
672                 goto DONE;
673             }
674
675             message_id = g_strdup_printf ("notmuch-sha1-%s", sha1);
676             free (sha1);
677         }
678
679         /* Now that we have a message ID, we get a message object,
680          * (which may or may not reference an existing document in the
681          * database). */
682
683         /* Use NULL for owner since we want to free this locally. */
684
685         /* XXX: This call can fail by either out-of-memory or an
686          * "impossible" Xapian exception. We should rewrite it to
687          * allow us to propagate the error status. */
688         message = _notmuch_message_create_for_message_id (NULL, notmuch,
689                                                           message_id);
690         if (message == NULL) {
691             fprintf (stderr, "Internal error. This shouldn't happen.\n\n");
692             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");
693             exit (1);
694         }
695
696         /* Has a message previously been added with the same ID? */
697         old_filename = notmuch_message_get_filename (message);
698         if (old_filename && strlen (old_filename)) {
699             ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
700             goto DONE;
701         } else {
702             _notmuch_message_set_filename (message, filename);
703             _notmuch_message_add_term (message, "type", "mail");
704         }
705
706         /* Next, find the thread(s) to which this message belongs. */
707         parents = g_ptr_array_new ();
708
709         refs = notmuch_message_file_get_header (message_file, "references");
710         parse_references (parents, refs);
711
712         in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
713         parse_references (parents, in_reply_to);
714
715         for (i = 0; i < parents->len; i++)
716             _notmuch_message_add_term (message, "ref",
717                                        (char *) g_ptr_array_index (parents, i));
718
719         thread_ids = find_thread_ids (notmuch, parents, message_id);
720
721         free (message_id);
722
723         for (i = 0; i < parents->len; i++)
724             g_free (g_ptr_array_index (parents, i));
725         g_ptr_array_free (parents, TRUE);
726
727         if (thread_ids->len) {
728             unsigned int i;
729             GString *thread_id;
730             char *id;
731
732             for (i = 0; i < thread_ids->len; i++) {
733                 id = (char *) thread_ids->pdata[i];
734                 _notmuch_message_add_thread_id (message, id);
735                 if (i == 0)
736                     thread_id = g_string_new (id);
737                 else
738                     g_string_append_printf (thread_id, ",%s", id);
739
740                 free (id);
741             }
742             g_string_free (thread_id, TRUE);
743         } else {
744             _notmuch_message_ensure_thread_id (message);
745         }
746
747         g_ptr_array_free (thread_ids, TRUE);
748
749         date = notmuch_message_file_get_header (message_file, "date");
750         _notmuch_message_set_date (message, date);
751
752         from = notmuch_message_file_get_header (message_file, "from");
753         subject = notmuch_message_file_get_header (message_file, "subject");
754         to = notmuch_message_file_get_header (message_file, "to");
755
756         if (from == NULL &&
757             subject == NULL &&
758             to == NULL)
759         {
760             ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
761             goto DONE;
762         } else {
763             _notmuch_message_sync (message);
764         }
765     } catch (const Xapian::Error &error) {
766         fprintf (stderr, "A Xapian exception occurred: %s.\n",
767                  error.get_msg().c_str());
768         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
769         goto DONE;
770     }
771
772   DONE:
773     if (message)
774         notmuch_message_destroy (message);
775     if (message_file)
776         notmuch_message_file_close (message_file);
777
778     return ret;
779 }