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