]> git.notmuchmail.org Git - notmuch/blob - lib/add-message.cc
lib/n_d_add_message: refactor test for new/ghost messages
[notmuch] / lib / add-message.cc
1 #include "database-private.h"
2
3 /* Advance 'str' past any whitespace or RFC 822 comments. A comment is
4  * a (potentially nested) parenthesized sequence with '\' used to
5  * escape any character (including parentheses).
6  *
7  * If the sequence to be skipped continues to the end of the string,
8  * then 'str' will be left pointing at the final terminating '\0'
9  * character.
10  */
11 static void
12 skip_space_and_comments (const char **str)
13 {
14     const char *s;
15
16     s = *str;
17     while (*s && (isspace (*s) || *s == '(')) {
18         while (*s && isspace (*s))
19             s++;
20         if (*s == '(') {
21             int nesting = 1;
22             s++;
23             while (*s && nesting) {
24                 if (*s == '(') {
25                     nesting++;
26                 } else if (*s == ')') {
27                     nesting--;
28                 } else if (*s == '\\') {
29                     if (*(s+1))
30                         s++;
31                 }
32                 s++;
33             }
34         }
35     }
36
37     *str = s;
38 }
39
40 /* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
41  * comments, and the '<' and '>' delimiters.
42  *
43  * If not NULL, then *next will be made to point to the first character
44  * not parsed, (possibly pointing to the final '\0' terminator.
45  *
46  * Returns a newly talloc'ed string belonging to 'ctx'.
47  *
48  * Returns NULL if there is any error parsing the message-id. */
49 static char *
50 _parse_message_id (void *ctx, const char *message_id, const char **next)
51 {
52     const char *s, *end;
53     char *result;
54
55     if (message_id == NULL || *message_id == '\0')
56         return NULL;
57
58     s = message_id;
59
60     skip_space_and_comments (&s);
61
62     /* Skip any unstructured text as well. */
63     while (*s && *s != '<')
64         s++;
65
66     if (*s == '<') {
67         s++;
68     } else {
69         if (next)
70             *next = s;
71         return NULL;
72     }
73
74     skip_space_and_comments (&s);
75
76     end = s;
77     while (*end && *end != '>')
78         end++;
79     if (next) {
80         if (*end)
81             *next = end + 1;
82         else
83             *next = end;
84     }
85
86     if (end > s && *end == '>')
87         end--;
88     if (end <= s)
89         return NULL;
90
91     result = talloc_strndup (ctx, s, end - s + 1);
92
93     /* Finally, collapse any whitespace that is within the message-id
94      * itself. */
95     {
96         char *r;
97         int len;
98
99         for (r = result, len = strlen (r); *r; r++, len--)
100             if (*r == ' ' || *r == '\t')
101                 memmove (r, r+1, len);
102     }
103
104     return result;
105 }
106
107 /* Parse a References header value, putting a (talloc'ed under 'ctx')
108  * copy of each referenced message-id into 'hash'.
109  *
110  * We explicitly avoid including any reference identical to
111  * 'message_id' in the result (to avoid mass confusion when a single
112  * message references itself cyclically---and yes, mail messages are
113  * not infrequent in the wild that do this---don't ask me why).
114  *
115  * Return the last reference parsed, if it is not equal to message_id.
116  */
117 static char *
118 parse_references (void *ctx,
119                   const char *message_id,
120                   GHashTable *hash,
121                   const char *refs)
122 {
123     char *ref, *last_ref = NULL;
124
125     if (refs == NULL || *refs == '\0')
126         return NULL;
127
128     while (*refs) {
129         ref = _parse_message_id (ctx, refs, &refs);
130
131         if (ref && strcmp (ref, message_id)) {
132             g_hash_table_add (hash, ref);
133             last_ref = ref;
134         }
135     }
136
137     /* The return value of this function is used to add a parent
138      * reference to the database.  We should avoid making a message
139      * its own parent, thus the above check.
140      */
141     return talloc_strdup(ctx, last_ref);
142 }
143
144 static const char *
145 _notmuch_database_generate_thread_id (notmuch_database_t *notmuch)
146 {
147     /* 16 bytes (+ terminator) for hexadecimal representation of
148      * a 64-bit integer. */
149     static char thread_id[17];
150     Xapian::WritableDatabase *db;
151
152     db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
153
154     notmuch->last_thread_id++;
155
156     sprintf (thread_id, "%016" PRIx64, notmuch->last_thread_id);
157
158     db->set_metadata ("last_thread_id", thread_id);
159
160     return thread_id;
161 }
162
163 static char *
164 _get_metadata_thread_id_key (void *ctx, const char *message_id)
165 {
166     if (strlen (message_id) > NOTMUCH_MESSAGE_ID_MAX)
167         message_id = _notmuch_message_id_compressed (ctx, message_id);
168
169     return talloc_asprintf (ctx, NOTMUCH_METADATA_THREAD_ID_PREFIX "%s",
170                             message_id);
171 }
172
173
174 static notmuch_status_t
175 _resolve_message_id_to_thread_id_old (notmuch_database_t *notmuch,
176                                       void *ctx,
177                                       const char *message_id,
178                                       const char **thread_id_ret);
179
180
181 /* Find the thread ID to which the message with 'message_id' belongs.
182  *
183  * Note: 'thread_id_ret' must not be NULL!
184  * On success '*thread_id_ret' is set to a newly talloced string belonging to
185  * 'ctx'.
186  *
187  * Note: If there is no message in the database with the given
188  * 'message_id' then a new thread_id will be allocated for this
189  * message ID and stored in the database metadata so that the
190  * thread ID can be looked up if the message is added to the database
191  * later.
192  */
193 static notmuch_status_t
194 _resolve_message_id_to_thread_id (notmuch_database_t *notmuch,
195                                   void *ctx,
196                                   const char *message_id,
197                                   const char **thread_id_ret)
198 {
199     notmuch_private_status_t status;
200     notmuch_message_t *message;
201
202     if (! (notmuch->features & NOTMUCH_FEATURE_GHOSTS))
203         return _resolve_message_id_to_thread_id_old (notmuch, ctx, message_id,
204                                                      thread_id_ret);
205
206     /* Look for this message (regular or ghost) */
207     message = _notmuch_message_create_for_message_id (
208         notmuch, message_id, &status);
209     if (status == NOTMUCH_PRIVATE_STATUS_SUCCESS) {
210         /* Message exists */
211         *thread_id_ret = talloc_steal (
212             ctx, notmuch_message_get_thread_id (message));
213     } else if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
214         /* Message did not exist.  Give it a fresh thread ID and
215          * populate this message as a ghost message. */
216         *thread_id_ret = talloc_strdup (
217             ctx, _notmuch_database_generate_thread_id (notmuch));
218         if (! *thread_id_ret) {
219             status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
220         } else {
221             status = _notmuch_message_initialize_ghost (message, *thread_id_ret);
222             if (status == 0)
223                 /* Commit the new ghost message */
224                 _notmuch_message_sync (message);
225         }
226     } else {
227         /* Create failed. Fall through. */
228     }
229
230     notmuch_message_destroy (message);
231
232     return COERCE_STATUS (status, "Error creating ghost message");
233 }
234
235 /* Pre-ghost messages _resolve_message_id_to_thread_id */
236 static notmuch_status_t
237 _resolve_message_id_to_thread_id_old (notmuch_database_t *notmuch,
238                                       void *ctx,
239                                       const char *message_id,
240                                       const char **thread_id_ret)
241 {
242     notmuch_status_t status;
243     notmuch_message_t *message;
244     std::string thread_id_string;
245     char *metadata_key;
246     Xapian::WritableDatabase *db;
247
248     status = notmuch_database_find_message (notmuch, message_id, &message);
249
250     if (status)
251         return status;
252
253     if (message) {
254         *thread_id_ret = talloc_steal (ctx,
255                                        notmuch_message_get_thread_id (message));
256
257         notmuch_message_destroy (message);
258
259         return NOTMUCH_STATUS_SUCCESS;
260     }
261
262     /* Message has not been seen yet.
263      *
264      * We may have seen a reference to it already, in which case, we
265      * can return the thread ID stored in the metadata. Otherwise, we
266      * generate a new thread ID and store it there.
267      */
268     db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
269     metadata_key = _get_metadata_thread_id_key (ctx, message_id);
270     thread_id_string = notmuch->xapian_db->get_metadata (metadata_key);
271
272     if (thread_id_string.empty()) {
273         *thread_id_ret = talloc_strdup (ctx,
274                                         _notmuch_database_generate_thread_id (notmuch));
275         db->set_metadata (metadata_key, *thread_id_ret);
276     } else {
277         *thread_id_ret = talloc_strdup (ctx, thread_id_string.c_str());
278     }
279
280     talloc_free (metadata_key);
281
282     return NOTMUCH_STATUS_SUCCESS;
283 }
284
285 static notmuch_status_t
286 _merge_threads (notmuch_database_t *notmuch,
287                 const char *winner_thread_id,
288                 const char *loser_thread_id)
289 {
290     Xapian::PostingIterator loser, loser_end;
291     notmuch_message_t *message = NULL;
292     notmuch_private_status_t private_status;
293     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
294
295     _notmuch_database_find_doc_ids (notmuch, "thread", loser_thread_id, &loser, &loser_end);
296
297     for ( ; loser != loser_end; loser++) {
298         message = _notmuch_message_create (notmuch, notmuch,
299                                            *loser, &private_status);
300         if (message == NULL) {
301             ret = COERCE_STATUS (private_status,
302                                  "Cannot find document for doc_id from query");
303             goto DONE;
304         }
305
306         _notmuch_message_remove_term (message, "thread", loser_thread_id);
307         _notmuch_message_add_term (message, "thread", winner_thread_id);
308         _notmuch_message_sync (message);
309
310         notmuch_message_destroy (message);
311         message = NULL;
312     }
313
314   DONE:
315     if (message)
316         notmuch_message_destroy (message);
317
318     return ret;
319 }
320
321 static void
322 _my_talloc_free_for_g_hash (void *ptr)
323 {
324     talloc_free (ptr);
325 }
326
327 static notmuch_status_t
328 _notmuch_database_link_message_to_parents (notmuch_database_t *notmuch,
329                                            notmuch_message_t *message,
330                                            notmuch_message_file_t *message_file,
331                                            const char **thread_id)
332 {
333     GHashTable *parents = NULL;
334     const char *refs, *in_reply_to, *in_reply_to_message_id;
335     const char *last_ref_message_id, *this_message_id;
336     GList *l, *keys = NULL;
337     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
338
339     parents = g_hash_table_new_full (g_str_hash, g_str_equal,
340                                      _my_talloc_free_for_g_hash, NULL);
341     this_message_id = notmuch_message_get_message_id (message);
342
343     refs = _notmuch_message_file_get_header (message_file, "references");
344     last_ref_message_id = parse_references (message,
345                                             this_message_id,
346                                             parents, refs);
347
348     in_reply_to = _notmuch_message_file_get_header (message_file, "in-reply-to");
349     in_reply_to_message_id = parse_references (message,
350                                                this_message_id,
351                                                parents, in_reply_to);
352
353     /* For the parent of this message, use the last message ID of the
354      * References header, if available.  If not, fall back to the
355      * first message ID in the In-Reply-To header. */
356     if (last_ref_message_id) {
357         _notmuch_message_add_term (message, "replyto",
358                                    last_ref_message_id);
359     } else if (in_reply_to_message_id) {
360         _notmuch_message_add_term (message, "replyto",
361                              in_reply_to_message_id);
362     }
363
364     keys = g_hash_table_get_keys (parents);
365     for (l = keys; l; l = l->next) {
366         char *parent_message_id;
367         const char *parent_thread_id = NULL;
368
369         parent_message_id = (char *) l->data;
370
371         _notmuch_message_add_term (message, "reference",
372                                    parent_message_id);
373
374         ret = _resolve_message_id_to_thread_id (notmuch,
375                                                 message,
376                                                 parent_message_id,
377                                                 &parent_thread_id);
378         if (ret)
379             goto DONE;
380
381         if (*thread_id == NULL) {
382             *thread_id = talloc_strdup (message, parent_thread_id);
383             _notmuch_message_add_term (message, "thread", *thread_id);
384         } else if (strcmp (*thread_id, parent_thread_id)) {
385             ret = _merge_threads (notmuch, *thread_id, parent_thread_id);
386             if (ret)
387                 goto DONE;
388         }
389     }
390
391   DONE:
392     if (keys)
393         g_list_free (keys);
394     if (parents)
395         g_hash_table_unref (parents);
396
397     return ret;
398 }
399
400 static notmuch_status_t
401 _notmuch_database_link_message_to_children (notmuch_database_t *notmuch,
402                                             notmuch_message_t *message,
403                                             const char **thread_id)
404 {
405     const char *message_id = notmuch_message_get_message_id (message);
406     Xapian::PostingIterator child, children_end;
407     notmuch_message_t *child_message = NULL;
408     const char *child_thread_id;
409     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
410     notmuch_private_status_t private_status;
411
412     _notmuch_database_find_doc_ids (notmuch, "reference", message_id, &child, &children_end);
413
414     for ( ; child != children_end; child++) {
415
416         child_message = _notmuch_message_create (message, notmuch,
417                                                  *child, &private_status);
418         if (child_message == NULL) {
419             ret = COERCE_STATUS (private_status,
420                                  "Cannot find document for doc_id from query");
421             goto DONE;
422         }
423
424         child_thread_id = notmuch_message_get_thread_id (child_message);
425         if (*thread_id == NULL) {
426             *thread_id = talloc_strdup (message, child_thread_id);
427             _notmuch_message_add_term (message, "thread", *thread_id);
428         } else if (strcmp (*thread_id, child_thread_id)) {
429             _notmuch_message_remove_term (child_message, "reference",
430                                           message_id);
431             _notmuch_message_sync (child_message);
432             ret = _merge_threads (notmuch, *thread_id, child_thread_id);
433             if (ret)
434                 goto DONE;
435         }
436
437         notmuch_message_destroy (child_message);
438         child_message = NULL;
439     }
440
441   DONE:
442     if (child_message)
443         notmuch_message_destroy (child_message);
444
445     return ret;
446 }
447
448 /* Fetch and clear the stored thread_id for message, or NULL if none. */
449 static char *
450 _consume_metadata_thread_id (void *ctx, notmuch_database_t *notmuch,
451                              notmuch_message_t *message)
452 {
453     const char *message_id;
454     std::string stored_id;
455     char *metadata_key;
456
457     message_id = notmuch_message_get_message_id (message);
458     metadata_key = _get_metadata_thread_id_key (ctx, message_id);
459
460     /* Check if we have already seen related messages to this one.
461      * If we have then use the thread_id that we stored at that time.
462      */
463     stored_id = notmuch->xapian_db->get_metadata (metadata_key);
464     if (stored_id.empty ()) {
465         return NULL;
466     } else {
467         Xapian::WritableDatabase *db;
468
469         db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
470
471         /* Clear the metadata for this message ID. We don't need it
472          * anymore. */
473         db->set_metadata (metadata_key, "");
474
475         return talloc_strdup (ctx, stored_id.c_str ());
476     }
477 }
478
479 /* Given a blank or ghost 'message' and its corresponding
480  * 'message_file' link it to existing threads in the database.
481  *
482  * First, if is_ghost, this retrieves the thread ID already stored in
483  * the message (which will be the case if a message was previously
484  * added that referenced this one).  If the message is blank
485  * (!is_ghost), it doesn't have a thread ID yet (we'll generate one
486  * later in this function).  If the database does not support ghost
487  * messages, this checks for a thread ID stored in database metadata
488  * for this message ID.
489  *
490  * Second, we look at 'message_file' and its link-relevant headers
491  * (References and In-Reply-To) for message IDs.
492  *
493  * Finally, we look in the database for existing message that
494  * reference 'message'.
495  *
496  * In all cases, we assign to the current message the first thread ID
497  * found. We will also merge any existing, distinct threads where this
498  * message belongs to both, (which is not uncommon when messages are
499  * processed out of order).
500  *
501  * Finally, if no thread ID has been found through referenced messages, we
502  * call _notmuch_message_generate_thread_id to generate a new thread
503  * ID. This should only happen for new, top-level messages, (no
504  * References or In-Reply-To header in this message, and no previously
505  * added message refers to this message).
506  */
507 static notmuch_status_t
508 _notmuch_database_link_message (notmuch_database_t *notmuch,
509                                 notmuch_message_t *message,
510                                 notmuch_message_file_t *message_file,
511                                 notmuch_bool_t is_ghost)
512 {
513     void *local = talloc_new (NULL);
514     notmuch_status_t status;
515     const char *thread_id = NULL;
516
517     /* Check if the message already had a thread ID */
518     if (notmuch->features & NOTMUCH_FEATURE_GHOSTS) {
519         if (is_ghost)
520             thread_id = notmuch_message_get_thread_id (message);
521     } else {
522         thread_id = _consume_metadata_thread_id (local, notmuch, message);
523         if (thread_id)
524             _notmuch_message_add_term (message, "thread", thread_id);
525     }
526
527     status = _notmuch_database_link_message_to_parents (notmuch, message,
528                                                         message_file,
529                                                         &thread_id);
530     if (status)
531         goto DONE;
532
533     if (! (notmuch->features & NOTMUCH_FEATURE_GHOSTS)) {
534         /* In general, it shouldn't be necessary to link children,
535          * since the earlier indexing of those children will have
536          * stored a thread ID for the missing parent.  However, prior
537          * to ghost messages, these stored thread IDs were NOT
538          * rewritten during thread merging (and there was no
539          * performant way to do so), so if indexed children were
540          * pulled into a different thread ID by a merge, it was
541          * necessary to pull them *back* into the stored thread ID of
542          * the parent.  With ghost messages, we just rewrite the
543          * stored thread IDs during merging, so this workaround isn't
544          * necessary. */
545         status = _notmuch_database_link_message_to_children (notmuch, message,
546                                                              &thread_id);
547         if (status)
548             goto DONE;
549     }
550
551     /* If not part of any existing thread, generate a new thread ID. */
552     if (thread_id == NULL) {
553         thread_id = _notmuch_database_generate_thread_id (notmuch);
554
555         _notmuch_message_add_term (message, "thread", thread_id);
556     }
557
558  DONE:
559     talloc_free (local);
560
561     return status;
562 }
563
564 notmuch_status_t
565 notmuch_database_add_message (notmuch_database_t *notmuch,
566                               const char *filename,
567                               notmuch_message_t **message_ret)
568 {
569     notmuch_message_file_t *message_file;
570     notmuch_message_t *message = NULL;
571     notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS, ret2;
572     notmuch_private_status_t private_status;
573     notmuch_bool_t is_ghost = FALSE, is_new = FALSE;
574
575     const char *date, *header;
576     const char *from, *to, *subject;
577     char *message_id = NULL;
578
579     if (message_ret)
580         *message_ret = NULL;
581
582     ret = _notmuch_database_ensure_writable (notmuch);
583     if (ret)
584         return ret;
585
586     message_file = _notmuch_message_file_open (notmuch, filename);
587     if (message_file == NULL)
588         return NOTMUCH_STATUS_FILE_ERROR;
589
590     /* Adding a message may change many documents.  Do this all
591      * atomically. */
592     ret = notmuch_database_begin_atomic (notmuch);
593     if (ret)
594         goto DONE;
595
596     /* Parse message up front to get better error status. */
597     ret = _notmuch_message_file_parse (message_file);
598     if (ret)
599         goto DONE;
600
601     /* Before we do any real work, (especially before doing a
602      * potential SHA-1 computation on the entire file's contents),
603      * let's make sure that what we're looking at looks like an
604      * actual email message.
605      */
606     from = _notmuch_message_file_get_header (message_file, "from");
607     subject = _notmuch_message_file_get_header (message_file, "subject");
608     to = _notmuch_message_file_get_header (message_file, "to");
609
610     if ((from == NULL || *from == '\0') &&
611         (subject == NULL || *subject == '\0') &&
612         (to == NULL || *to == '\0')) {
613         ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
614         goto DONE;
615     }
616
617     /* Now that we're sure it's mail, the first order of business
618      * is to find a message ID (or else create one ourselves).
619      */
620     header = _notmuch_message_file_get_header (message_file, "message-id");
621     if (header && *header != '\0') {
622         message_id = _parse_message_id (message_file, header, NULL);
623
624         /* So the header value isn't RFC-compliant, but it's
625          * better than no message-id at all.
626          */
627         if (message_id == NULL)
628             message_id = talloc_strdup (message_file, header);
629     }
630
631     if (message_id == NULL ) {
632         /* No message-id at all, let's generate one by taking a
633          * hash over the file's contents.
634          */
635         char *sha1 = _notmuch_sha1_of_file (filename);
636
637         /* If that failed too, something is really wrong. Give up. */
638         if (sha1 == NULL) {
639             ret = NOTMUCH_STATUS_FILE_ERROR;
640             goto DONE;
641         }
642
643         message_id = talloc_asprintf (message_file, "notmuch-sha1-%s", sha1);
644         free (sha1);
645     }
646
647     try {
648         /* Now that we have a message ID, we get a message object,
649          * (which may or may not reference an existing document in the
650          * database). */
651
652         message = _notmuch_message_create_for_message_id (notmuch,
653                                                           message_id,
654                                                           &private_status);
655
656         talloc_free (message_id);
657
658         /* We cannot call notmuch_message_get_flag for a new message */
659         switch (private_status) {
660         case NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND:
661             is_ghost = FALSE;
662             is_new = TRUE;
663             break;
664         case NOTMUCH_PRIVATE_STATUS_SUCCESS:
665             is_ghost = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_GHOST);
666             is_new = FALSE;
667             break;
668         default:
669             ret = COERCE_STATUS (private_status,
670                                  "Unexpected status value from _notmuch_message_create_for_message_id");
671             goto DONE;
672         }
673
674         _notmuch_message_add_filename (message, filename);
675
676         if (is_new || is_ghost) {
677             _notmuch_message_add_term (message, "type", "mail");
678             if (is_ghost)
679                 /* Convert ghost message to a regular message */
680                 _notmuch_message_remove_term (message, "type", "ghost");
681             ret = _notmuch_database_link_message (notmuch, message,
682                                                   message_file, is_ghost);
683             if (ret)
684                 goto DONE;
685
686             date = _notmuch_message_file_get_header (message_file, "date");
687             _notmuch_message_set_header_values (message, date, from, subject);
688
689             ret = _notmuch_message_index_file (message, message_file);
690             if (ret)
691                 goto DONE;
692         } else {
693             ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
694         }
695
696         _notmuch_message_sync (message);
697     } catch (const Xapian::Error &error) {
698         _notmuch_database_log (notmuch, "A Xapian exception occurred adding message: %s.\n",
699                  error.get_msg().c_str());
700         notmuch->exception_reported = TRUE;
701         ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
702         goto DONE;
703     }
704
705   DONE:
706     if (message) {
707         if ((ret == NOTMUCH_STATUS_SUCCESS ||
708              ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) && message_ret)
709             *message_ret = message;
710         else
711             notmuch_message_destroy (message);
712     }
713
714     if (message_file)
715         _notmuch_message_file_close (message_file);
716
717     ret2 = notmuch_database_end_atomic (notmuch);
718     if ((ret == NOTMUCH_STATUS_SUCCESS ||
719          ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) &&
720         ret2 != NOTMUCH_STATUS_SUCCESS)
721         ret = ret2;
722
723     return ret;
724 }