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