1 /**
2  * @file
3  * Notmuch virtual mailbox type
4  *
5  * @authors
6  * Copyright (C) 2011-2016 Karel Zak <kzak@redhat.com>
7  * Copyright (C) 2016-2018 Richard Russon <rich@flatcap.org>
8  * Copyright (C) 2016 Kevin Velghe <kevin@paretje.be>
9  * Copyright (C) 2017 Bernard 'Guyzmo' Pratz <guyzmo+github+pub@m0g.net>
10  *
11  * @copyright
12  * This program is free software: you can redistribute it and/or modify it under
13  * the terms of the GNU General Public License as published by the Free Software
14  * Foundation, either version 2 of the License, or (at your option) any later
15  * version.
16  *
17  * This program is distributed in the hope that it will be useful, but WITHOUT
18  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
19  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
20  * details.
21  *
22  * You should have received a copy of the GNU General Public License along with
23  * this program.  If not, see <http://www.gnu.org/licenses/>.
24  */
25 
26 /**
27  * @page nm_notmuch Notmuch virtual mailbox type
28  *
29  * Notmuch virtual mailbox type
30  *
31  * ## Notes
32  *
33  * - notmuch uses private Mailbox->data and private Email->data
34  *
35  * - all exported functions are usable within notmuch context only
36  *
37  * - all functions have to be covered by "mailbox->type == MUTT_NOTMUCH" check
38  *   (it's implemented in nm_mdata_get() and init_mailbox() functions).
39  *
40  * - exception are nm_nonctx_* functions -- these functions use nm_default_url
41  *   (or parse URL from another resource)
42  *
43  * Implementation: #MxNotmuchOps
44  */
45 
46 #include "config.h"
47 #include <errno.h>
48 #include <limits.h>
49 #include <notmuch.h>
50 #include <stdbool.h>
51 #include <stdint.h>
52 #include <stdio.h>
53 #include <string.h>
54 #include <time.h>
55 #include <unistd.h>
56 #include "private.h"
57 #include "mutt/lib.h"
58 #include "config/lib.h"
59 #include "email/lib.h"
60 #include "core/lib.h"
61 #include "gui/lib.h"
62 #include "mutt.h"
63 #include "lib.h"
64 #include "hcache/lib.h"
65 #include "index/lib.h"
66 #include "maildir/lib.h"
67 #include "progress/lib.h"
68 #include "adata.h"
69 #include "command_parse.h"
70 #include "edata.h"
71 #include "maildir/edata.h"
72 #include "mdata.h"
73 #include "mutt_commands.h"
74 #include "mutt_globals.h"
75 #include "mutt_thread.h"
76 #include "mx.h"
77 #include "protos.h"
78 #include "query.h"
79 #include "tag.h"
80 #ifdef ENABLE_NLS
81 #include <libintl.h>
82 #endif
83 
84 struct stat;
85 
86 static const struct Command nm_commands[] = {
87   // clang-format off
88   { "unvirtual-mailboxes", parse_unmailboxes, 0 },
89   { "virtual-mailboxes",   parse_mailboxes,   MUTT_NAMED },
90   // clang-format on
91 };
92 
93 const char NmUrlProtocol[] = "notmuch://";
94 const int NmUrlProtocolLen = sizeof(NmUrlProtocol) - 1;
95 
96 /**
97  * nm_init - Setup feature commands
98  */
nm_init(void)99 void nm_init(void)
100 {
101   COMMANDS_REGISTER(nm_commands);
102 }
103 
104 /**
105  * nm_hcache_open - Open a header cache
106  * @param m Mailbox
107  * @retval ptr Header cache handle
108  */
nm_hcache_open(struct Mailbox * m)109 static struct HeaderCache *nm_hcache_open(struct Mailbox *m)
110 {
111 #ifdef USE_HCACHE
112   const char *const c_header_cache =
113       cs_subset_path(NeoMutt->sub, "header_cache");
114   return mutt_hcache_open(c_header_cache, mailbox_path(m), NULL);
115 #else
116   return NULL;
117 #endif
118 }
119 
120 /**
121  * nm_hcache_close - Close the header cache
122  * @param h Header cache handle
123  */
nm_hcache_close(struct HeaderCache * h)124 static void nm_hcache_close(struct HeaderCache *h)
125 {
126 #ifdef USE_HCACHE
127   mutt_hcache_close(h);
128 #endif
129 }
130 
131 /**
132  * nm_get_default_url - Create a Mailbox with default Notmuch settings
133  * @retval ptr  Mailbox with default Notmuch settings
134  * @retval NULL Error, it's impossible to create an NmMboxData
135  */
nm_get_default_url(void)136 static char *nm_get_default_url(void)
137 {
138   // path to DB + query + url "decoration"
139   size_t len = PATH_MAX + 1024 + 32;
140   char *url = mutt_mem_malloc(len);
141 
142   // Try to use `$nm_default_url` or `$folder`.
143   // If neither are set, it is impossible to create a Notmuch URL.
144   const char *const c_nm_default_url =
145       cs_subset_string(NeoMutt->sub, "nm_default_url");
146   const char *const c_folder = cs_subset_string(NeoMutt->sub, "folder");
147   if (c_nm_default_url)
148     snprintf(url, len, "%s", c_nm_default_url);
149   else if (c_folder)
150     snprintf(url, len, "notmuch://%s", c_folder);
151   else
152   {
153     FREE(&url);
154     return NULL;
155   }
156 
157   return url;
158 }
159 
160 /**
161  * nm_get_default_data - Create a Mailbox with default Notmuch settings
162  * @retval ptr  Mailbox with default Notmuch settings
163  * @retval NULL Error, it's impossible to create an NmMboxData
164  */
nm_get_default_data(void)165 static struct NmMboxData *nm_get_default_data(void)
166 {
167   // path to DB + query + url "decoration"
168   char *url = nm_get_default_url();
169   if (!url)
170     return NULL;
171 
172   struct NmMboxData *default_data = nm_mdata_new(url);
173   FREE(&url);
174 
175   return default_data;
176 }
177 
178 /**
179  * init_mailbox - Add Notmuch data to the Mailbox
180  * @param m Mailbox
181  * @retval  0 Success
182  * @retval -1 Error Bad format
183  *
184  * Create a new NmMboxData struct and add it Mailbox::data.
185  * Notmuch-specific data will be stored in this struct.
186  * This struct can be freed using nm_mdata_free().
187  */
init_mailbox(struct Mailbox * m)188 static int init_mailbox(struct Mailbox *m)
189 {
190   if (!m || (m->type != MUTT_NOTMUCH))
191     return -1;
192 
193   if (m->mdata)
194     return 0;
195 
196   m->mdata = nm_mdata_new(mailbox_path(m));
197   if (!m->mdata)
198     return -1;
199 
200   m->mdata_free = nm_mdata_free;
201   return 0;
202 }
203 
204 /**
205  * email_get_id - Get the unique Notmuch Id
206  * @param e Email
207  * @retval ptr  ID string
208  * @retval NULL Error
209  */
email_get_id(struct Email * e)210 static char *email_get_id(struct Email *e)
211 {
212   struct NmEmailData *edata = nm_edata_get(e);
213   if (!edata)
214     return NULL;
215 
216   return edata->virtual_id;
217 }
218 
219 /**
220  * email_get_fullpath - Get the full path of an email
221  * @param e      Email
222  * @param buf    Buffer for the path
223  * @param buflen Length of the buffer
224  * @retval ptr Path string
225  */
email_get_fullpath(struct Email * e,char * buf,size_t buflen)226 static char *email_get_fullpath(struct Email *e, char *buf, size_t buflen)
227 {
228   snprintf(buf, buflen, "%s/%s", nm_email_get_folder(e), e->path);
229   return buf;
230 }
231 
232 /**
233  * query_window_reset - Restore vfolder's search window to its original position
234  *
235  * After moving a vfolder search window backward and forward, calling this function
236  * will reset the search position to its original value, setting to 0 the user settable
237  * variable:
238  *
239  *     nm_query_window_current_position
240  */
query_window_reset(void)241 static void query_window_reset(void)
242 {
243   mutt_debug(LL_DEBUG2, "entering\n");
244   cs_subset_str_native_set(NeoMutt->sub, "nm_query_window_current_position", 0, NULL);
245 }
246 
247 /**
248  * windowed_query_from_query - Transforms a vfolder search query into a windowed one
249  * @param[in]  query vfolder search string
250  * @param[out] buf   allocated string buffer to receive the modified search query
251  * @param[in]  buflen allocated maximum size of the buf string buffer
252  * @retval true  Transformed search query is available as a string in buf
253  * @retval false Search query shall not be transformed
254  *
255  * Creates a `date:` search term window from the following user settings:
256  *
257  * - `nm_query_window_enable` (only required for `nm_query_window_duration = 0`)
258  * - `nm_query_window_duration`
259  * - `nm_query_window_timebase`
260  * - `nm_query_window_current_position`
261  *
262  * The window won't be applied:
263  *
264  * - If the duration of the search query is set to `0` this function will be
265  *   disabled unless a user explicitly enables windowed queries.
266  * - If the timebase is invalid, it will show an error message and do nothing.
267  *
268  * If there's no search registered in `nm_query_window_current_search` or this is
269  * a new search, it will reset the window and do the search.
270  */
windowed_query_from_query(const char * query,char * buf,size_t buflen)271 static bool windowed_query_from_query(const char *query, char *buf, size_t buflen)
272 {
273   mutt_debug(LL_DEBUG2, "nm: %s\n", query);
274 
275   const bool c_nm_query_window_enable =
276       cs_subset_bool(NeoMutt->sub, "nm_query_window_enable");
277   const short c_nm_query_window_duration =
278       cs_subset_number(NeoMutt->sub, "nm_query_window_duration");
279   const short c_nm_query_window_current_position =
280       cs_subset_number(NeoMutt->sub, "nm_query_window_current_position");
281   const char *const c_nm_query_window_current_search =
282       cs_subset_string(NeoMutt->sub, "nm_query_window_current_search");
283   const char *const c_nm_query_window_timebase =
284       cs_subset_string(NeoMutt->sub, "nm_query_window_timebase");
285   const char *const c_nm_query_window_or_terms =
286       cs_subset_string(NeoMutt->sub, "nm_query_window_or_terms");
287 
288   /* if the query has changed, reset the window position */
289   if (!c_nm_query_window_current_search ||
290       (strcmp(query, c_nm_query_window_current_search) != 0))
291   {
292     query_window_reset();
293   }
294 
295   enum NmWindowQueryRc rc = nm_windowed_query_from_query(
296       buf, buflen, c_nm_query_window_enable, c_nm_query_window_duration,
297       c_nm_query_window_current_position, c_nm_query_window_current_search,
298       c_nm_query_window_timebase, c_nm_query_window_or_terms);
299 
300   switch (rc)
301   {
302     case NM_WINDOW_QUERY_SUCCESS:
303     {
304       mutt_debug(LL_DEBUG2, "nm: %s -> %s\n", query, buf);
305       break;
306     }
307     case NM_WINDOW_QUERY_INVALID_DURATION:
308     {
309       query_window_reset();
310       return false;
311     }
312     case NM_WINDOW_QUERY_INVALID_TIMEBASE:
313     {
314       mutt_message(
315           // L10N: The values 'hour', 'day', 'week', 'month', 'year' are literal.
316           //       They should not be translated.
317           _("Invalid nm_query_window_timebase value (valid values are: "
318             "hour, day, week, month, year)"));
319       mutt_debug(LL_DEBUG2, "Invalid nm_query_window_timebase value\n");
320       return false;
321     }
322   }
323 
324   return true;
325 }
326 
327 /**
328  * get_query_string - Builds the notmuch vfolder search string
329  * @param mdata Notmuch Mailbox data
330  * @param window If true enable application of the window on the search string
331  * @retval ptr  String containing a notmuch search query
332  * @retval NULL None can be generated
333  *
334  * This function parses the internal representation of a search, and returns
335  * a search query string ready to be fed to the notmuch API, given the search
336  * is valid.
337  *
338  * @note The window parameter here is here to decide contextually whether we
339  * want to return a search query with window applied (for the actual search
340  * result in mailbox) or not (for the count in the sidebar). It is not aimed at
341  * enabling/disabling the feature.
342  */
get_query_string(struct NmMboxData * mdata,bool window)343 static char *get_query_string(struct NmMboxData *mdata, bool window)
344 {
345   mutt_debug(LL_DEBUG2, "nm: %s\n", window ? "true" : "false");
346 
347   if (!mdata)
348     return NULL;
349   if (mdata->db_query && !window)
350     return mdata->db_query;
351 
352   const char *const c_nm_query_type =
353       cs_subset_string(NeoMutt->sub, "nm_query_type");
354   mdata->query_type = nm_string_to_query_type(c_nm_query_type); /* user's default */
355 
356   struct UrlQuery *item = NULL;
357   STAILQ_FOREACH(item, &mdata->db_url->query_strings, entries)
358   {
359     if (!item->value || !item->name)
360       continue;
361 
362     if (strcmp(item->name, "limit") == 0)
363     {
364       if (mutt_str_atoi(item->value, &mdata->db_limit))
365         mutt_error(_("failed to parse notmuch limit: %s"), item->value);
366     }
367     else if (strcmp(item->name, "type") == 0)
368       mdata->query_type = nm_string_to_query_type(item->value);
369     else if (strcmp(item->name, "query") == 0)
370       mutt_str_replace(&mdata->db_query, item->value);
371   }
372 
373   if (!mdata->db_query)
374     return NULL;
375 
376   if (window)
377   {
378     char buf[1024];
379     cs_subset_str_string_set(NeoMutt->sub, "nm_query_window_current_search",
380                              mdata->db_query, NULL);
381 
382     /* if a date part is defined, do not apply windows (to avoid the risk of
383      * having a non-intersected date frame). A good improvement would be to
384      * accept if they intersect */
385     if (!strstr(mdata->db_query, "date:") &&
386         windowed_query_from_query(mdata->db_query, buf, sizeof(buf)))
387     {
388       mutt_str_replace(&mdata->db_query, buf);
389     }
390 
391     mutt_debug(LL_DEBUG2, "nm: query (windowed) '%s'\n", mdata->db_query);
392   }
393   else
394     mutt_debug(LL_DEBUG2, "nm: query '%s'\n", mdata->db_query);
395 
396   return mdata->db_query;
397 }
398 
399 /**
400  * get_limit - Get the database limit
401  * @param mdata Notmuch Mailbox data
402  * @retval num Current limit
403  */
get_limit(struct NmMboxData * mdata)404 static int get_limit(struct NmMboxData *mdata)
405 {
406   return mdata ? mdata->db_limit : 0;
407 }
408 
409 /**
410  * apply_exclude_tags - Exclude the configured tags
411  * @param query Notmuch query
412  */
apply_exclude_tags(notmuch_query_t * query)413 static void apply_exclude_tags(notmuch_query_t *query)
414 {
415   const char *const c_nm_exclude_tags =
416       cs_subset_string(NeoMutt->sub, "nm_exclude_tags");
417   if (!c_nm_exclude_tags || !query)
418     return;
419 
420   struct TagArray tags = nm_tag_str_to_tags(c_nm_exclude_tags);
421 
422   char **tag = NULL;
423   ARRAY_FOREACH(tag, &tags.tags)
424   {
425     mutt_debug(LL_DEBUG2, "nm: query exclude tag '%s'\n", *tag);
426     notmuch_query_add_tag_exclude(query, *tag);
427   }
428 
429   notmuch_query_set_omit_excluded(query, 1);
430   nm_tag_array_free(&tags);
431 }
432 
433 /**
434  * get_query - Create a new query
435  * @param m        Mailbox
436  * @param writable Should the query be updateable?
437  * @retval ptr  Notmuch query
438  * @retval NULL Error
439  */
get_query(struct Mailbox * m,bool writable)440 static notmuch_query_t *get_query(struct Mailbox *m, bool writable)
441 {
442   struct NmMboxData *mdata = nm_mdata_get(m);
443   if (!mdata)
444     return NULL;
445 
446   notmuch_database_t *db = nm_db_get(m, writable);
447   const char *str = get_query_string(mdata, true);
448 
449   if (!db || !str)
450     goto err;
451 
452   notmuch_query_t *q = notmuch_query_create(db, str);
453   if (!q)
454     goto err;
455 
456   apply_exclude_tags(q);
457   notmuch_query_set_sort(q, NOTMUCH_SORT_NEWEST_FIRST);
458   mutt_debug(LL_DEBUG2, "nm: query successfully initialized (%s)\n", str);
459   return q;
460 err:
461   nm_db_release(m);
462   return NULL;
463 }
464 
465 /**
466  * update_email_tags - Update the Email's tags from Notmuch
467  * @param e   Email
468  * @param msg Notmuch message
469  * @retval 0 Success
470  * @retval 1 Tags unchanged
471  */
update_email_tags(struct Email * e,notmuch_message_t * msg)472 static int update_email_tags(struct Email *e, notmuch_message_t *msg)
473 {
474   struct NmEmailData *edata = nm_edata_get(e);
475   char *new_tags = NULL;
476   char *old_tags = NULL;
477 
478   mutt_debug(LL_DEBUG2, "nm: tags update requested (%s)\n", edata->virtual_id);
479 
480   for (notmuch_tags_t *tags = notmuch_message_get_tags(msg);
481        tags && notmuch_tags_valid(tags); notmuch_tags_move_to_next(tags))
482   {
483     const char *t = notmuch_tags_get(tags);
484     if (!t || (*t == '\0'))
485       continue;
486 
487     mutt_str_append_item(&new_tags, t, ' ');
488   }
489 
490   old_tags = driver_tags_get(&e->tags);
491 
492   if (new_tags && old_tags && (strcmp(old_tags, new_tags) == 0))
493   {
494     FREE(&old_tags);
495     FREE(&new_tags);
496     mutt_debug(LL_DEBUG2, "nm: tags unchanged\n");
497     return 1;
498   }
499   FREE(&old_tags);
500 
501   /* new version */
502   driver_tags_replace(&e->tags, new_tags);
503   FREE(&new_tags);
504 
505   new_tags = driver_tags_get_transformed(&e->tags);
506   mutt_debug(LL_DEBUG2, "nm: new tags: '%s'\n", new_tags);
507   FREE(&new_tags);
508 
509   new_tags = driver_tags_get(&e->tags);
510   mutt_debug(LL_DEBUG2, "nm: new tag transforms: '%s'\n", new_tags);
511   FREE(&new_tags);
512 
513   return 0;
514 }
515 
516 /**
517  * update_message_path - Set the path for a message
518  * @param e    Email
519  * @param path Path
520  * @retval 0 Success
521  * @retval 1 Failure
522  */
update_message_path(struct Email * e,const char * path)523 static int update_message_path(struct Email *e, const char *path)
524 {
525   struct NmEmailData *edata = nm_edata_get(e);
526 
527   mutt_debug(LL_DEBUG2, "nm: path update requested path=%s, (%s)\n", path, edata->virtual_id);
528 
529   char *p = strrchr(path, '/');
530   if (p && ((p - path) > 3) &&
531       (mutt_strn_equal(p - 3, "cur", 3) || mutt_strn_equal(p - 3, "new", 3) ||
532        mutt_strn_equal(p - 3, "tmp", 3)))
533   {
534     edata->type = MUTT_MAILDIR;
535 
536     FREE(&e->path);
537     FREE(&edata->folder);
538 
539     p -= 3; /* skip subfolder (e.g. "new") */
540     if (cs_subset_bool(NeoMutt->sub, "mark_old"))
541     {
542       e->old = mutt_str_startswith(p, "cur");
543     }
544     e->path = mutt_str_dup(p);
545 
546     for (; (p > path) && (*(p - 1) == '/'); p--)
547       ; // do nothing
548 
549     edata->folder = mutt_strn_dup(path, p - path);
550 
551     mutt_debug(LL_DEBUG2, "nm: folder='%s', file='%s'\n", edata->folder, e->path);
552     return 0;
553   }
554 
555   return 1;
556 }
557 
558 /**
559  * get_folder_from_path - Find an email's folder from its path
560  * @param path Path
561  * @retval ptr  Path string
562  * @retval NULL Error
563  */
get_folder_from_path(const char * path)564 static char *get_folder_from_path(const char *path)
565 {
566   char *p = strrchr(path, '/');
567 
568   if (p && ((p - path) > 3) &&
569       (mutt_strn_equal(p - 3, "cur", 3) || mutt_strn_equal(p - 3, "new", 3) ||
570        mutt_strn_equal(p - 3, "tmp", 3)))
571   {
572     p -= 3;
573     for (; (p > path) && (*(p - 1) == '/'); p--)
574       ; // do nothing
575 
576     return mutt_strn_dup(path, p - path);
577   }
578 
579   return NULL;
580 }
581 
582 /**
583  * nm2mutt_message_id - Converts notmuch message Id to neomutt message Id
584  * @param id Notmuch ID to convert
585  * @retval ptr NeoMutt message ID
586  *
587  * Caller must free the NeoMutt Message ID
588  */
nm2mutt_message_id(const char * id)589 static char *nm2mutt_message_id(const char *id)
590 {
591   size_t sz;
592   char *mid = NULL;
593 
594   if (!id)
595     return NULL;
596   sz = strlen(id) + 3;
597   mid = mutt_mem_malloc(sz);
598 
599   snprintf(mid, sz, "<%s>", id);
600   return mid;
601 }
602 
603 /**
604  * init_email - Set up an email's Notmuch data
605  * @param e    Email
606  * @param path Path to email
607  * @param msg  Notmuch message
608  * @retval  0 Success
609  * @retval -1 Failure
610  */
init_email(struct Email * e,const char * path,notmuch_message_t * msg)611 static int init_email(struct Email *e, const char *path, notmuch_message_t *msg)
612 {
613   if (nm_edata_get(e))
614     return 0;
615 
616   struct NmEmailData *edata = nm_edata_new();
617   e->nm_edata = edata;
618 
619   /* Notmuch ensures that message Id exists (if not notmuch Notmuch will
620    * generate an ID), so it's more safe than use neomutt Email->env->id */
621   const char *id = notmuch_message_get_message_id(msg);
622   edata->virtual_id = mutt_str_dup(id);
623 
624   mutt_debug(LL_DEBUG2, "nm: [e=%p, edata=%p] (%s)\n", (void *) e, (void *) edata, id);
625 
626   char *nm_msg_id = nm2mutt_message_id(id);
627   if (!e->env->message_id)
628   {
629     e->env->message_id = nm_msg_id;
630   }
631   else if (!mutt_str_equal(e->env->message_id, nm_msg_id))
632   {
633     FREE(&e->env->message_id);
634     e->env->message_id = nm_msg_id;
635   }
636   else
637   {
638     FREE(&nm_msg_id);
639   }
640 
641   if (update_message_path(e, path) != 0)
642     return -1;
643 
644   update_email_tags(e, msg);
645 
646   return 0;
647 }
648 
649 /**
650  * get_message_last_filename - Get a message's last filename
651  * @param msg Notmuch message
652  * @retval ptr  Filename
653  * @retval NULL Error
654  */
get_message_last_filename(notmuch_message_t * msg)655 static const char *get_message_last_filename(notmuch_message_t *msg)
656 {
657   const char *name = NULL;
658 
659   for (notmuch_filenames_t *ls = notmuch_message_get_filenames(msg);
660        ls && notmuch_filenames_valid(ls); notmuch_filenames_move_to_next(ls))
661   {
662     name = notmuch_filenames_get(ls);
663   }
664 
665   return name;
666 }
667 
668 /**
669  * progress_setup - Set up the Progress Bar
670  * @param m Mailbox
671  */
progress_setup(struct Mailbox * m)672 static void progress_setup(struct Mailbox *m)
673 {
674   if (!m->verbose)
675     return;
676 
677   struct NmMboxData *mdata = nm_mdata_get(m);
678   if (!mdata)
679     return;
680 
681   mdata->oldmsgcount = m->msg_count;
682   mdata->ignmsgcount = 0;
683   mdata->progress =
684       progress_new(_("Reading messages..."), MUTT_PROGRESS_READ, mdata->oldmsgcount);
685 }
686 
687 /**
688  * nm_progress_update - Update the progress counter
689  * @param m Mailbox
690  */
nm_progress_update(struct Mailbox * m)691 static void nm_progress_update(struct Mailbox *m)
692 {
693   struct NmMboxData *mdata = nm_mdata_get(m);
694 
695   if (!m->verbose || !mdata || !mdata->progress)
696     return;
697 
698   progress_update(mdata->progress, m->msg_count + mdata->ignmsgcount, -1);
699 }
700 
701 /**
702  * get_mutt_email - Get the Email of a Notmuch message
703  * @param m Mailbox
704  * @param msg Notmuch message
705  * @retval ptr  Email
706  * @retval NULL Error
707  */
get_mutt_email(struct Mailbox * m,notmuch_message_t * msg)708 static struct Email *get_mutt_email(struct Mailbox *m, notmuch_message_t *msg)
709 {
710   if (!m || !msg)
711     return NULL;
712 
713   const char *id = notmuch_message_get_message_id(msg);
714   if (!id)
715     return NULL;
716 
717   mutt_debug(LL_DEBUG2, "nm: neomutt email, id='%s'\n", id);
718 
719   if (!m->id_hash)
720   {
721     mutt_debug(LL_DEBUG2, "nm: init hash\n");
722     m->id_hash = mutt_make_id_hash(m);
723     if (!m->id_hash)
724       return NULL;
725   }
726 
727   char *mid = nm2mutt_message_id(id);
728   mutt_debug(LL_DEBUG2, "nm: neomutt id='%s'\n", mid);
729 
730   struct Email *e = mutt_hash_find(m->id_hash, mid);
731   FREE(&mid);
732   return e;
733 }
734 
735 /**
736  * append_message - Associate a message
737  * @param h     Header cache handle
738  * @param m     Mailbox
739  * @param msg   Notmuch message
740  * @param dedup De-duplicate results
741  */
append_message(struct HeaderCache * h,struct Mailbox * m,notmuch_message_t * msg,bool dedup)742 static void append_message(struct HeaderCache *h, struct Mailbox *m,
743                            notmuch_message_t *msg, bool dedup)
744 {
745   struct NmMboxData *mdata = nm_mdata_get(m);
746   if (!mdata)
747     return;
748 
749   char *newpath = NULL;
750   struct Email *e = NULL;
751 
752   /* deduplicate */
753   if (dedup && get_mutt_email(m, msg))
754   {
755     mdata->ignmsgcount++;
756     nm_progress_update(m);
757     mutt_debug(LL_DEBUG2, "nm: ignore id=%s, already in the m\n",
758                notmuch_message_get_message_id(msg));
759     return;
760   }
761 
762   const char *path = get_message_last_filename(msg);
763   if (!path)
764     return;
765 
766   mutt_debug(LL_DEBUG2, "nm: appending message, i=%d, id=%s, path=%s\n",
767              m->msg_count, notmuch_message_get_message_id(msg), path);
768 
769   if (m->msg_count >= m->email_max)
770   {
771     mutt_debug(LL_DEBUG2, "nm: allocate mx memory\n");
772     mx_alloc_memory(m);
773   }
774 
775 #ifdef USE_HCACHE
776   e = mutt_hcache_fetch(h, path, mutt_str_len(path), 0).email;
777   if (!e)
778 #endif
779   {
780     if (access(path, F_OK) == 0)
781     {
782       /* We pass is_old=false as argument here, but e->old will be updated later
783        * by update_message_path() (called by init_email() below).  */
784       e = maildir_parse_message(MUTT_MAILDIR, path, false, NULL);
785     }
786     else
787     {
788       /* maybe moved try find it... */
789       char *folder = get_folder_from_path(path);
790 
791       if (folder)
792       {
793         FILE *fp = maildir_open_find_message(folder, path, &newpath);
794         if (fp)
795         {
796           e = maildir_parse_stream(MUTT_MAILDIR, fp, newpath, false, NULL);
797           mutt_file_fclose(&fp);
798 
799           mutt_debug(LL_DEBUG1, "nm: not up-to-date: %s -> %s\n", path, newpath);
800         }
801       }
802       FREE(&folder);
803     }
804 
805     if (!e)
806     {
807       mutt_debug(LL_DEBUG1, "nm: failed to parse message: %s\n", path);
808       goto done;
809     }
810 
811 #ifdef USE_HCACHE
812     mutt_hcache_store(h, newpath ? newpath : path,
813                       mutt_str_len(newpath ? newpath : path), e, 0);
814 #endif
815   }
816 
817   if (init_email(e, newpath ? newpath : path, msg) != 0)
818   {
819     email_free(&e);
820     mutt_debug(LL_DEBUG1, "nm: failed to append email!\n");
821     goto done;
822   }
823 
824   e->active = true;
825   e->index = m->msg_count;
826   mailbox_size_add(m, e);
827   m->emails[m->msg_count] = e;
828   m->msg_count++;
829 
830   if (newpath)
831   {
832     /* remember that file has been moved -- nm_mbox_sync() will update the DB */
833     struct NmEmailData *edata = nm_edata_get(e);
834     if (edata)
835     {
836       mutt_debug(LL_DEBUG1, "nm: remember obsolete path: %s\n", path);
837       edata->oldpath = mutt_str_dup(path);
838     }
839   }
840   nm_progress_update(m);
841 done:
842   FREE(&newpath);
843 }
844 
845 /**
846  * append_replies - Add all the replies to a given messages into the display
847  * @param h     Header cache handle
848  * @param m     Mailbox
849  * @param q     Notmuch query
850  * @param top   Notmuch message
851  * @param dedup De-duplicate the results
852  *
853  * Careful, this calls itself recursively to make sure we get everything.
854  */
append_replies(struct HeaderCache * h,struct Mailbox * m,notmuch_query_t * q,notmuch_message_t * top,bool dedup)855 static void append_replies(struct HeaderCache *h, struct Mailbox *m,
856                            notmuch_query_t *q, notmuch_message_t *top, bool dedup)
857 {
858   notmuch_messages_t *msgs = NULL;
859 
860   for (msgs = notmuch_message_get_replies(top); notmuch_messages_valid(msgs);
861        notmuch_messages_move_to_next(msgs))
862   {
863     notmuch_message_t *nm = notmuch_messages_get(msgs);
864     append_message(h, m, nm, dedup);
865     /* recurse through all the replies to this message too */
866     append_replies(h, m, q, nm, dedup);
867     notmuch_message_destroy(nm);
868   }
869 }
870 
871 /**
872  * append_thread - Add each top level reply in the thread
873  * @param h      Header cache handle
874  * @param m      Mailbox
875  * @param q      Notmuch query
876  * @param thread Notmuch thread
877  * @param dedup  De-duplicate the results
878  *
879  * add each top level reply in the thread, and then add each reply to the top
880  * level replies
881  */
append_thread(struct HeaderCache * h,struct Mailbox * m,notmuch_query_t * q,notmuch_thread_t * thread,bool dedup)882 static void append_thread(struct HeaderCache *h, struct Mailbox *m,
883                           notmuch_query_t *q, notmuch_thread_t *thread, bool dedup)
884 {
885   notmuch_messages_t *msgs = NULL;
886 
887   for (msgs = notmuch_thread_get_toplevel_messages(thread);
888        notmuch_messages_valid(msgs); notmuch_messages_move_to_next(msgs))
889   {
890     notmuch_message_t *nm = notmuch_messages_get(msgs);
891     append_message(h, m, nm, dedup);
892     append_replies(h, m, q, nm, dedup);
893     notmuch_message_destroy(nm);
894   }
895 }
896 
897 /**
898  * get_messages - Load messages for a query
899  * @param query Notmuch query
900  * @retval ptr  Messages matching query
901  * @retval NULL Error occurred
902  *
903  * This helper method is to be the single point for retrieving messages. It
904  * handles version specific calls, which will make maintenance easier.
905  */
get_messages(notmuch_query_t * query)906 static notmuch_messages_t *get_messages(notmuch_query_t *query)
907 {
908   if (!query)
909     return NULL;
910 
911   notmuch_messages_t *msgs = NULL;
912 
913 #if LIBNOTMUCH_CHECK_VERSION(5, 0, 0)
914   if (notmuch_query_search_messages(query, &msgs) != NOTMUCH_STATUS_SUCCESS)
915     return NULL;
916 #elif LIBNOTMUCH_CHECK_VERSION(4, 3, 0)
917   if (notmuch_query_search_messages_st(query, &msgs) != NOTMUCH_STATUS_SUCCESS)
918     return NULL;
919 #else
920   msgs = notmuch_query_search_messages(query);
921 #endif
922 
923   return msgs;
924 }
925 
926 /**
927  * read_mesgs_query - Search for matching messages
928  * @param m     Mailbox
929  * @param q     Notmuch query
930  * @param dedup De-duplicate the results
931  * @retval true  Success
932  * @retval false Failure
933  */
read_mesgs_query(struct Mailbox * m,notmuch_query_t * q,bool dedup)934 static bool read_mesgs_query(struct Mailbox *m, notmuch_query_t *q, bool dedup)
935 {
936   struct NmMboxData *mdata = nm_mdata_get(m);
937   if (!mdata)
938     return false;
939 
940   int limit = get_limit(mdata);
941 
942   notmuch_messages_t *msgs = get_messages(q);
943 
944   if (!msgs)
945     return false;
946 
947   struct HeaderCache *h = nm_hcache_open(m);
948 
949   for (; notmuch_messages_valid(msgs) && ((limit == 0) || (m->msg_count < limit));
950        notmuch_messages_move_to_next(msgs))
951   {
952     if (SigInt)
953     {
954       nm_hcache_close(h);
955       SigInt = false;
956       return false;
957     }
958     notmuch_message_t *nm = notmuch_messages_get(msgs);
959     append_message(h, m, nm, dedup);
960     notmuch_message_destroy(nm);
961   }
962 
963   nm_hcache_close(h);
964   return true;
965 }
966 
967 /**
968  * get_threads - Load threads for a query
969  * @param query Notmuch query
970  * @retval ptr Threads matching query
971  * @retval NULL Error occurred
972  *
973  * This helper method is to be the single point for retrieving messages. It
974  * handles version specific calls, which will make maintenance easier.
975  */
get_threads(notmuch_query_t * query)976 static notmuch_threads_t *get_threads(notmuch_query_t *query)
977 {
978   if (!query)
979     return NULL;
980 
981   notmuch_threads_t *threads = NULL;
982 #if LIBNOTMUCH_CHECK_VERSION(5, 0, 0)
983   if (notmuch_query_search_threads(query, &threads) != NOTMUCH_STATUS_SUCCESS)
984     return false;
985 #elif LIBNOTMUCH_CHECK_VERSION(4, 3, 0)
986   if (notmuch_query_search_threads_st(query, &threads) != NOTMUCH_STATUS_SUCCESS)
987     return false;
988 #else
989   threads = notmuch_query_search_threads(query);
990 #endif
991 
992   return threads;
993 }
994 
995 /**
996  * read_threads_query - Perform a query with threads
997  * @param m     Mailbox
998  * @param q     Query type
999  * @param dedup Should the results be de-duped?
1000  * @param limit Maximum number of results
1001  * @retval true  Success
1002  * @retval false Failure
1003  */
read_threads_query(struct Mailbox * m,notmuch_query_t * q,bool dedup,int limit)1004 static bool read_threads_query(struct Mailbox *m, notmuch_query_t *q, bool dedup, int limit)
1005 {
1006   struct NmMboxData *mdata = nm_mdata_get(m);
1007   if (!mdata)
1008     return false;
1009 
1010   notmuch_threads_t *threads = get_threads(q);
1011   if (!threads)
1012     return false;
1013 
1014   struct HeaderCache *h = nm_hcache_open(m);
1015 
1016   for (; notmuch_threads_valid(threads) && ((limit == 0) || (m->msg_count < limit));
1017        notmuch_threads_move_to_next(threads))
1018   {
1019     if (SigInt)
1020     {
1021       nm_hcache_close(h);
1022       SigInt = false;
1023       return false;
1024     }
1025     notmuch_thread_t *thread = notmuch_threads_get(threads);
1026     append_thread(h, m, q, thread, dedup);
1027     notmuch_thread_destroy(thread);
1028   }
1029 
1030   nm_hcache_close(h);
1031   return true;
1032 }
1033 
1034 /**
1035  * get_nm_message - Find a Notmuch message
1036  * @param db  Notmuch database
1037  * @param e Email
1038  * @retval ptr Handle to the Notmuch message
1039  */
get_nm_message(notmuch_database_t * db,struct Email * e)1040 static notmuch_message_t *get_nm_message(notmuch_database_t *db, struct Email *e)
1041 {
1042   notmuch_message_t *msg = NULL;
1043   char *id = email_get_id(e);
1044 
1045   mutt_debug(LL_DEBUG2, "nm: find message (%s)\n", id);
1046 
1047   if (id && db)
1048     notmuch_database_find_message(db, id, &msg);
1049 
1050   return msg;
1051 }
1052 
1053 /**
1054  * nm_message_has_tag - Does a message have this tag?
1055  * @param msg Notmuch message
1056  * @param tag Tag
1057  * @retval true It does
1058  */
nm_message_has_tag(notmuch_message_t * msg,char * tag)1059 static bool nm_message_has_tag(notmuch_message_t *msg, char *tag)
1060 {
1061   const char *possible_match_tag = NULL;
1062   notmuch_tags_t *tags = NULL;
1063 
1064   for (tags = notmuch_message_get_tags(msg); notmuch_tags_valid(tags);
1065        notmuch_tags_move_to_next(tags))
1066   {
1067     possible_match_tag = notmuch_tags_get(tags);
1068     if (mutt_str_equal(possible_match_tag, tag))
1069     {
1070       return true;
1071     }
1072   }
1073   return false;
1074 }
1075 
1076 /**
1077  * sync_email_path_with_nm - Synchronize Neomutt's Email path with notmuch.
1078  * @param e Email in Neomutt
1079  * @param msg Email from notmuch
1080  */
sync_email_path_with_nm(struct Email * e,notmuch_message_t * msg)1081 static void sync_email_path_with_nm(struct Email *e, notmuch_message_t *msg)
1082 {
1083   const char *new_file = get_message_last_filename(msg);
1084   char old_file[PATH_MAX];
1085   email_get_fullpath(e, old_file, sizeof(old_file));
1086 
1087   if (!mutt_str_equal(old_file, new_file))
1088     update_message_path(e, new_file);
1089 }
1090 
1091 /**
1092  * update_tags - Update the tags on a message
1093  * @param msg     Notmuch message
1094  * @param tag_str String of tags (space separated)
1095  * @retval  0 Success
1096  * @retval -1 Failure
1097  */
update_tags(notmuch_message_t * msg,const char * tag_str)1098 static int update_tags(notmuch_message_t *msg, const char *tag_str)
1099 {
1100   if (!tag_str)
1101     return -1;
1102 
1103   notmuch_message_freeze(msg);
1104 
1105   struct TagArray tags = nm_tag_str_to_tags(tag_str);
1106   char **tag_elem = NULL;
1107   ARRAY_FOREACH(tag_elem, &tags.tags)
1108   {
1109     char *tag = *tag_elem;
1110 
1111     if (tag[0] == '-')
1112     {
1113       mutt_debug(LL_DEBUG1, "nm: remove tag: '%s'\n", tag + 1);
1114       notmuch_message_remove_tag(msg, tag + 1);
1115     }
1116     else if (tag[0] == '!')
1117     {
1118       mutt_debug(LL_DEBUG1, "nm: toggle tag: '%s'\n", tag + 1);
1119       if (nm_message_has_tag(msg, tag + 1))
1120       {
1121         notmuch_message_remove_tag(msg, tag + 1);
1122       }
1123       else
1124       {
1125         notmuch_message_add_tag(msg, tag + 1);
1126       }
1127     }
1128     else
1129     {
1130       mutt_debug(LL_DEBUG1, "nm: add tag: '%s'\n", (tag[0] == '+') ? tag + 1 : tag);
1131       notmuch_message_add_tag(msg, (tag[0] == '+') ? tag + 1 : tag);
1132     }
1133   }
1134 
1135   notmuch_message_thaw(msg);
1136   nm_tag_array_free(&tags);
1137 
1138   return 0;
1139 }
1140 
1141 /**
1142  * update_email_flags - Update the Email's flags
1143  * @param m       Mailbox
1144  * @param e       Email
1145  * @param tag_str String of tags (space separated)
1146  * @retval  0 Success
1147  * @retval -1 Failure
1148  *
1149  * TODO: join update_email_tags and update_email_flags, which are given an
1150  * array of tags.
1151  */
update_email_flags(struct Mailbox * m,struct Email * e,const char * tag_str)1152 static int update_email_flags(struct Mailbox *m, struct Email *e, const char *tag_str)
1153 {
1154   if (!tag_str)
1155     return -1;
1156 
1157   const char *const c_nm_unread_tag =
1158       cs_subset_string(NeoMutt->sub, "nm_unread_tag");
1159   const char *const c_nm_replied_tag =
1160       cs_subset_string(NeoMutt->sub, "nm_replied_tag");
1161   const char *const c_nm_flagged_tag =
1162       cs_subset_string(NeoMutt->sub, "nm_flagged_tag");
1163 
1164   struct TagArray tags = nm_tag_str_to_tags(tag_str);
1165   char **tag_elem = NULL;
1166   ARRAY_FOREACH(tag_elem, &tags.tags)
1167   {
1168     char *tag = *tag_elem;
1169 
1170     if (tag[0] == '-')
1171     {
1172       tag++;
1173       if (strcmp(tag, c_nm_unread_tag) == 0)
1174         mutt_set_flag(m, e, MUTT_READ, true);
1175       else if (strcmp(tag, c_nm_replied_tag) == 0)
1176         mutt_set_flag(m, e, MUTT_REPLIED, false);
1177       else if (strcmp(tag, c_nm_flagged_tag) == 0)
1178         mutt_set_flag(m, e, MUTT_FLAG, false);
1179     }
1180     else
1181     {
1182       tag = (tag[0] == '+') ? tag + 1 : tag;
1183       if (strcmp(tag, c_nm_unread_tag) == 0)
1184         mutt_set_flag(m, e, MUTT_READ, false);
1185       else if (strcmp(tag, c_nm_replied_tag) == 0)
1186         mutt_set_flag(m, e, MUTT_REPLIED, true);
1187       else if (strcmp(tag, c_nm_flagged_tag) == 0)
1188         mutt_set_flag(m, e, MUTT_FLAG, true);
1189     }
1190   }
1191 
1192   nm_tag_array_free(&tags);
1193 
1194   return 0;
1195 }
1196 
1197 /**
1198  * rename_maildir_filename - Rename a Maildir file
1199  * @param old    Old path
1200  * @param buf    Buffer for new path
1201  * @param buflen Length of buffer
1202  * @param e      Email
1203  * @retval  0 Success, renamed
1204  * @retval  1 Success, no change
1205  * @retval -1 Failure
1206  */
rename_maildir_filename(const char * old,char * buf,size_t buflen,struct Email * e)1207 static int rename_maildir_filename(const char *old, char *buf, size_t buflen, struct Email *e)
1208 {
1209   char filename[PATH_MAX];
1210   char suffix[PATH_MAX];
1211   char folder[PATH_MAX];
1212 
1213   mutt_str_copy(folder, old, sizeof(folder));
1214   char *p = strrchr(folder, '/');
1215   if (p)
1216   {
1217     *p = '\0';
1218     p++;
1219   }
1220   else
1221     p = folder;
1222 
1223   mutt_str_copy(filename, p, sizeof(filename));
1224 
1225   /* remove (new,cur,...) from folder path */
1226   p = strrchr(folder, '/');
1227   if (p)
1228     *p = '\0';
1229 
1230   /* remove old flags from filename */
1231   p = strchr(filename, ':');
1232   if (p)
1233     *p = '\0';
1234 
1235   /* compose new flags */
1236   maildir_gen_flags(suffix, sizeof(suffix), e);
1237 
1238   snprintf(buf, buflen, "%s/%s/%s%s", folder,
1239            (e->read || e->old) ? "cur" : "new", filename, suffix);
1240 
1241   if (strcmp(old, buf) == 0)
1242     return 1;
1243 
1244   if (rename(old, buf) != 0)
1245   {
1246     mutt_debug(LL_DEBUG1, "nm: rename(2) failed %s -> %s\n", old, buf);
1247     return -1;
1248   }
1249 
1250   return 0;
1251 }
1252 
1253 /**
1254  * remove_filename - Delete a file
1255  * @param m Mailbox
1256  * @param path Path of file
1257  * @retval  0 Success
1258  * @retval -1 Failure
1259  */
remove_filename(struct Mailbox * m,const char * path)1260 static int remove_filename(struct Mailbox *m, const char *path)
1261 {
1262   struct NmMboxData *mdata = nm_mdata_get(m);
1263   if (!mdata)
1264     return -1;
1265 
1266   mutt_debug(LL_DEBUG2, "nm: remove filename '%s'\n", path);
1267 
1268   notmuch_database_t *db = nm_db_get(m, true);
1269   if (!db)
1270     return -1;
1271 
1272   notmuch_message_t *msg = NULL;
1273   notmuch_status_t st = notmuch_database_find_message_by_filename(db, path, &msg);
1274   if (st || !msg)
1275     return -1;
1276 
1277   int trans = nm_db_trans_begin(m);
1278   if (trans < 0)
1279     return -1;
1280 
1281   /* note that unlink() is probably unnecessary here, it's already removed
1282    * by mh_sync_mailbox_message(), but for sure...  */
1283   notmuch_filenames_t *ls = NULL;
1284   st = notmuch_database_remove_message(db, path);
1285   switch (st)
1286   {
1287     case NOTMUCH_STATUS_SUCCESS:
1288       mutt_debug(LL_DEBUG2, "nm: remove success, call unlink\n");
1289       unlink(path);
1290       break;
1291     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
1292       mutt_debug(LL_DEBUG2, "nm: remove success (duplicate), call unlink\n");
1293       unlink(path);
1294       for (ls = notmuch_message_get_filenames(msg);
1295            ls && notmuch_filenames_valid(ls); notmuch_filenames_move_to_next(ls))
1296       {
1297         path = notmuch_filenames_get(ls);
1298 
1299         mutt_debug(LL_DEBUG2, "nm: remove duplicate: '%s'\n", path);
1300         unlink(path);
1301         notmuch_database_remove_message(db, path);
1302       }
1303       break;
1304     default:
1305       mutt_debug(LL_DEBUG1, "nm: failed to remove '%s' [st=%d]\n", path, (int) st);
1306       break;
1307   }
1308 
1309   notmuch_message_destroy(msg);
1310   if (trans)
1311     nm_db_trans_end(m);
1312   return 0;
1313 }
1314 
1315 /**
1316  * rename_filename - Rename the file
1317  * @param m        Notmuch Mailbox data
1318  * @param old_file Old filename
1319  * @param new_file New filename
1320  * @param e        Email
1321  * @retval  0      Success
1322  * @retval -1      Failure
1323  */
rename_filename(struct Mailbox * m,const char * old_file,const char * new_file,struct Email * e)1324 static int rename_filename(struct Mailbox *m, const char *old_file,
1325                            const char *new_file, struct Email *e)
1326 {
1327   struct NmMboxData *mdata = nm_mdata_get(m);
1328   if (!mdata)
1329     return -1;
1330 
1331   notmuch_database_t *db = nm_db_get(m, true);
1332   if (!db || !new_file || !old_file || (access(new_file, F_OK) != 0))
1333     return -1;
1334 
1335   int rc = -1;
1336   notmuch_status_t st;
1337   notmuch_filenames_t *ls = NULL;
1338   notmuch_message_t *msg = NULL;
1339 
1340   mutt_debug(LL_DEBUG1, "nm: rename filename, %s -> %s\n", old_file, new_file);
1341   int trans = nm_db_trans_begin(m);
1342   if (trans < 0)
1343     return -1;
1344 
1345   mutt_debug(LL_DEBUG2, "nm: rename: add '%s'\n", new_file);
1346 #if LIBNOTMUCH_CHECK_VERSION(5, 1, 0)
1347   st = notmuch_database_index_file(db, new_file, NULL, &msg);
1348 #else
1349   st = notmuch_database_add_message(db, new_file, &msg);
1350 #endif
1351 
1352   if ((st != NOTMUCH_STATUS_SUCCESS) && (st != NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID))
1353   {
1354     mutt_debug(LL_DEBUG1, "nm: failed to add '%s' [st=%d]\n", new_file, (int) st);
1355     goto done;
1356   }
1357 
1358   mutt_debug(LL_DEBUG2, "nm: rename: rem '%s'\n", old_file);
1359   st = notmuch_database_remove_message(db, old_file);
1360   switch (st)
1361   {
1362     case NOTMUCH_STATUS_SUCCESS:
1363       break;
1364     case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
1365       mutt_debug(LL_DEBUG2, "nm: rename: syncing duplicate filename\n");
1366       notmuch_message_destroy(msg);
1367       msg = NULL;
1368       notmuch_database_find_message_by_filename(db, new_file, &msg);
1369 
1370       for (ls = notmuch_message_get_filenames(msg);
1371            msg && ls && notmuch_filenames_valid(ls); notmuch_filenames_move_to_next(ls))
1372       {
1373         const char *path = notmuch_filenames_get(ls);
1374         char newpath[PATH_MAX];
1375 
1376         if (strcmp(new_file, path) == 0)
1377           continue;
1378 
1379         mutt_debug(LL_DEBUG2, "nm: rename: syncing duplicate: %s\n", path);
1380 
1381         if (rename_maildir_filename(path, newpath, sizeof(newpath), e) == 0)
1382         {
1383           mutt_debug(LL_DEBUG2, "nm: rename dup %s -> %s\n", path, newpath);
1384           notmuch_database_remove_message(db, path);
1385 #if LIBNOTMUCH_CHECK_VERSION(5, 1, 0)
1386           notmuch_database_index_file(db, newpath, NULL, NULL);
1387 #else
1388           notmuch_database_add_message(db, newpath, NULL);
1389 #endif
1390         }
1391       }
1392       notmuch_message_destroy(msg);
1393       msg = NULL;
1394       notmuch_database_find_message_by_filename(db, new_file, &msg);
1395       st = NOTMUCH_STATUS_SUCCESS;
1396       break;
1397     default:
1398       mutt_debug(LL_DEBUG1, "nm: failed to remove '%s' [st=%d]\n", old_file, (int) st);
1399       break;
1400   }
1401 
1402   if ((st == NOTMUCH_STATUS_SUCCESS) && e && msg)
1403   {
1404     notmuch_message_maildir_flags_to_tags(msg);
1405     update_email_tags(e, msg);
1406 
1407     char *tags = driver_tags_get(&e->tags);
1408     update_tags(msg, tags);
1409     FREE(&tags);
1410   }
1411 
1412   rc = 0;
1413 done:
1414   if (msg)
1415     notmuch_message_destroy(msg);
1416   if (trans)
1417     nm_db_trans_end(m);
1418   return rc;
1419 }
1420 
1421 /**
1422  * count_query - Count the results of a query
1423  * @param db    Notmuch database
1424  * @param qstr  Query to execute
1425  * @param limit Maximum number of results
1426  * @retval num Number of results
1427  */
count_query(notmuch_database_t * db,const char * qstr,int limit)1428 static unsigned int count_query(notmuch_database_t *db, const char *qstr, int limit)
1429 {
1430   notmuch_query_t *q = notmuch_query_create(db, qstr);
1431   if (!q)
1432     return 0;
1433 
1434   unsigned int res = 0;
1435 
1436   apply_exclude_tags(q);
1437 #if LIBNOTMUCH_CHECK_VERSION(5, 0, 0)
1438   if (notmuch_query_count_messages(q, &res) != NOTMUCH_STATUS_SUCCESS)
1439     res = 0; /* may not be defined on error */
1440 #elif LIBNOTMUCH_CHECK_VERSION(4, 3, 0)
1441   if (notmuch_query_count_messages_st(q, &res) != NOTMUCH_STATUS_SUCCESS)
1442     res = 0; /* may not be defined on error */
1443 #else
1444   res = notmuch_query_count_messages(q);
1445 #endif
1446   notmuch_query_destroy(q);
1447   mutt_debug(LL_DEBUG1, "nm: count '%s', result=%d\n", qstr, res);
1448 
1449   if ((limit > 0) && (res > limit))
1450     res = limit;
1451 
1452   return res;
1453 }
1454 
1455 /**
1456  * nm_email_get_folder - Get the folder for a Email
1457  * @param e Email
1458  * @retval ptr  Folder containing email
1459  * @retval NULL Error
1460  */
nm_email_get_folder(struct Email * e)1461 char *nm_email_get_folder(struct Email *e)
1462 {
1463   struct NmEmailData *edata = nm_edata_get(e);
1464   if (!edata)
1465     return NULL;
1466 
1467   return edata->folder;
1468 }
1469 
1470 /**
1471  * nm_email_get_folder_rel_db - Get the folder for a Email from the same level as the notmuch database
1472  * @param m Mailbox containing Email
1473  * @param e Email
1474  * @retval ptr  Folder containing email from the same level as the notmuch db
1475  * @retval NULL Error
1476  *
1477  * Instead of returning a path like /var/mail/account/Inbox, this returns
1478  * account/Inbox. If wanting the full path, use nm_email_get_folder().
1479  */
nm_email_get_folder_rel_db(struct Mailbox * m,struct Email * e)1480 char *nm_email_get_folder_rel_db(struct Mailbox *m, struct Email *e)
1481 {
1482   char *full_folder = nm_email_get_folder(e);
1483   if (!full_folder)
1484     return NULL;
1485 
1486   const char *db_path = nm_db_get_filename(m);
1487   if (!db_path)
1488     return NULL;
1489 
1490   return full_folder + strlen(db_path);
1491 }
1492 
1493 /**
1494  * nm_read_entire_thread - Get the entire thread of an email
1495  * @param m Mailbox
1496  * @param e   Email
1497  * @retval  0 Success
1498  * @retval -1 Failure
1499  */
nm_read_entire_thread(struct Mailbox * m,struct Email * e)1500 int nm_read_entire_thread(struct Mailbox *m, struct Email *e)
1501 {
1502   if (!m)
1503     return -1;
1504 
1505   struct NmMboxData *mdata = nm_mdata_get(m);
1506   if (!mdata)
1507     return -1;
1508 
1509   notmuch_query_t *q = NULL;
1510   notmuch_database_t *db = NULL;
1511   notmuch_message_t *msg = NULL;
1512   int rc = -1;
1513 
1514   if (!(db = nm_db_get(m, false)) || !(msg = get_nm_message(db, e)))
1515     goto done;
1516 
1517   mutt_debug(LL_DEBUG1, "nm: reading entire-thread messages...[current count=%d]\n",
1518              m->msg_count);
1519 
1520   progress_setup(m);
1521   const char *id = notmuch_message_get_thread_id(msg);
1522   if (!id)
1523     goto done;
1524 
1525   char *qstr = NULL;
1526   mutt_str_append_item(&qstr, "thread:", '\0');
1527   mutt_str_append_item(&qstr, id, '\0');
1528 
1529   q = notmuch_query_create(db, qstr);
1530   FREE(&qstr);
1531   if (!q)
1532     goto done;
1533   apply_exclude_tags(q);
1534   notmuch_query_set_sort(q, NOTMUCH_SORT_NEWEST_FIRST);
1535 
1536   read_threads_query(m, q, true, 0);
1537   m->mtime.tv_sec = mutt_date_epoch();
1538   m->mtime.tv_nsec = 0;
1539   rc = 0;
1540 
1541   if (m->msg_count > mdata->oldmsgcount)
1542     mailbox_changed(m, NT_MAILBOX_INVALID);
1543 done:
1544   if (q)
1545     notmuch_query_destroy(q);
1546 
1547   nm_db_release(m);
1548 
1549   if (m->msg_count == mdata->oldmsgcount)
1550     mutt_message(_("No more messages in the thread"));
1551 
1552   mdata->oldmsgcount = 0;
1553   mutt_debug(LL_DEBUG1, "nm: reading entire-thread messages... done [rc=%d, count=%d]\n",
1554              rc, m->msg_count);
1555   progress_free(&mdata->progress);
1556   return rc;
1557 }
1558 
1559 /**
1560  * nm_url_from_query - Turn a query into a URL
1561  * @param m      Mailbox
1562  * @param buf    Buffer for URL
1563  * @param buflen Length of buffer
1564  * @retval ptr  Query as a URL
1565  * @retval NULL Error
1566  */
nm_url_from_query(struct Mailbox * m,char * buf,size_t buflen)1567 char *nm_url_from_query(struct Mailbox *m, char *buf, size_t buflen)
1568 {
1569   mutt_debug(LL_DEBUG2, "(%s)\n", buf);
1570   struct NmMboxData *mdata = nm_mdata_get(m);
1571   char url[PATH_MAX + 1024 + 32]; /* path to DB + query + URL "decoration" */
1572   int added;
1573   bool using_default_data = false;
1574 
1575   // No existing data. Try to get a default NmMboxData.
1576   if (!mdata)
1577   {
1578     mdata = nm_get_default_data();
1579 
1580     // Failed to get default data.
1581     if (!mdata)
1582       return NULL;
1583 
1584     using_default_data = true;
1585   }
1586 
1587   mdata->query_type = nm_parse_type_from_query(buf);
1588 
1589   const short c_nm_db_limit = cs_subset_number(NeoMutt->sub, "nm_db_limit");
1590   if (get_limit(mdata) == c_nm_db_limit)
1591   {
1592     added = snprintf(url, sizeof(url), "%s%s?type=%s&query=", NmUrlProtocol,
1593                      nm_db_get_filename(m), nm_query_type_to_string(mdata->query_type));
1594   }
1595   else
1596   {
1597     added = snprintf(url, sizeof(url), "%s%s?type=%s&limit=%d&query=", NmUrlProtocol,
1598                      nm_db_get_filename(m),
1599                      nm_query_type_to_string(mdata->query_type), get_limit(mdata));
1600   }
1601 
1602   if (added >= sizeof(url))
1603   {
1604     // snprintf output was truncated, so can't create URL
1605     return NULL;
1606   }
1607 
1608   url_pct_encode(&url[added], sizeof(url) - added, buf);
1609 
1610   mutt_str_copy(buf, url, buflen);
1611   buf[buflen - 1] = '\0';
1612 
1613   if (using_default_data)
1614     nm_mdata_free((void **) &mdata);
1615 
1616   mutt_debug(LL_DEBUG1, "nm: url from query '%s'\n", buf);
1617   return buf;
1618 }
1619 
1620 /**
1621  * nm_query_window_available - Are windowed queries enabled for use?
1622  * @retval true Windowed queries in use
1623  */
nm_query_window_available(void)1624 bool nm_query_window_available(void)
1625 {
1626   const short c_nm_query_window_duration =
1627       cs_subset_number(NeoMutt->sub, "nm_query_window_duration");
1628   const bool c_nm_query_window_enable =
1629       cs_subset_bool(NeoMutt->sub, "nm_query_window_enable");
1630 
1631   return c_nm_query_window_enable || (c_nm_query_window_duration > 0);
1632 }
1633 
1634 /**
1635  * nm_query_window_forward - Function to move the current search window forward in time
1636  *
1637  * Updates `nm_query_window_current_position` by decrementing it by 1, or does nothing
1638  * if the current window already is set to 0.
1639  *
1640  * The lower the value of `nm_query_window_current_position` is, the more recent the
1641  * result will be.
1642  */
nm_query_window_forward(void)1643 void nm_query_window_forward(void)
1644 {
1645   const short c_nm_query_window_current_position =
1646       cs_subset_number(NeoMutt->sub, "nm_query_window_current_position");
1647   if (c_nm_query_window_current_position != 0)
1648   {
1649     cs_subset_str_native_set(NeoMutt->sub, "nm_query_window_current_position",
1650                              c_nm_query_window_current_position - 1, NULL);
1651   }
1652 
1653   mutt_debug(LL_DEBUG2, "(%d)\n", c_nm_query_window_current_position - 1);
1654 }
1655 
1656 /**
1657  * nm_query_window_backward - Function to move the current search window backward in time
1658  *
1659  * Updates `nm_query_window_current_position` by incrementing it by 1
1660  *
1661  * The higher the value of `nm_query_window_current_position` is, the less recent the
1662  * result will be.
1663  */
nm_query_window_backward(void)1664 void nm_query_window_backward(void)
1665 {
1666   const short c_nm_query_window_current_position =
1667       cs_subset_number(NeoMutt->sub, "nm_query_window_current_position");
1668   cs_subset_str_native_set(NeoMutt->sub, "nm_query_window_current_position",
1669                            c_nm_query_window_current_position + 1, NULL);
1670   mutt_debug(LL_DEBUG2, "(%d)\n", c_nm_query_window_current_position + 1);
1671 }
1672 
1673 /**
1674  * nm_query_window_reset - Resets the vfolder window position to the present.
1675  */
nm_query_window_reset(void)1676 void nm_query_window_reset(void)
1677 {
1678   cs_subset_str_native_set(NeoMutt->sub, "nm_query_window_current_position", 0, NULL);
1679   mutt_debug(LL_DEBUG2, "Reset nm_query_window_current_position to 0\n");
1680 }
1681 
1682 /**
1683  * nm_message_is_still_queried - Is a message still visible in the query?
1684  * @param m Mailbox
1685  * @param e Email
1686  * @retval true Message is still in query
1687  */
nm_message_is_still_queried(struct Mailbox * m,struct Email * e)1688 bool nm_message_is_still_queried(struct Mailbox *m, struct Email *e)
1689 {
1690   struct NmMboxData *mdata = nm_mdata_get(m);
1691   notmuch_database_t *db = nm_db_get(m, false);
1692   char *orig_str = get_query_string(mdata, true);
1693 
1694   if (!db || !orig_str)
1695     return false;
1696 
1697   char *new_str = NULL;
1698   bool rc = false;
1699   if (mutt_str_asprintf(&new_str, "id:%s and (%s)", email_get_id(e), orig_str) < 0)
1700     return false;
1701 
1702   mutt_debug(LL_DEBUG2, "nm: checking if message is still queried: %s\n", new_str);
1703 
1704   notmuch_query_t *q = notmuch_query_create(db, new_str);
1705 
1706   switch (mdata->query_type)
1707   {
1708     case NM_QUERY_TYPE_UNKNOWN: // UNKNOWN should never occur, but MESGS is default
1709     case NM_QUERY_TYPE_MESGS:
1710     {
1711       notmuch_messages_t *messages = get_messages(q);
1712 
1713       if (!messages)
1714         return false;
1715 
1716       rc = notmuch_messages_valid(messages);
1717       notmuch_messages_destroy(messages);
1718       break;
1719     }
1720     case NM_QUERY_TYPE_THREADS:
1721     {
1722       notmuch_threads_t *threads = get_threads(q);
1723 
1724       if (!threads)
1725         return false;
1726 
1727       rc = notmuch_threads_valid(threads);
1728       notmuch_threads_destroy(threads);
1729       break;
1730     }
1731   }
1732 
1733   notmuch_query_destroy(q);
1734 
1735   mutt_debug(LL_DEBUG2, "nm: checking if message is still queried: %s = %s\n",
1736              new_str, rc ? "true" : "false");
1737 
1738   return rc;
1739 }
1740 
1741 /**
1742  * nm_update_filename - Change the filename
1743  * @param m        Mailbox
1744  * @param old_file Old filename
1745  * @param new_file New filename
1746  * @param e        Email
1747  * @retval  0      Success
1748  * @retval -1      Failure
1749  */
nm_update_filename(struct Mailbox * m,const char * old_file,const char * new_file,struct Email * e)1750 int nm_update_filename(struct Mailbox *m, const char *old_file,
1751                        const char *new_file, struct Email *e)
1752 {
1753   char buf[PATH_MAX];
1754   struct NmMboxData *mdata = nm_mdata_get(m);
1755   if (!mdata || !new_file)
1756     return -1;
1757 
1758   if (!old_file && nm_edata_get(e))
1759   {
1760     email_get_fullpath(e, buf, sizeof(buf));
1761     old_file = buf;
1762   }
1763 
1764   int rc = rename_filename(m, old_file, new_file, e);
1765 
1766   nm_db_release(m);
1767   m->mtime.tv_sec = mutt_date_epoch();
1768   m->mtime.tv_nsec = 0;
1769   return rc;
1770 }
1771 
1772 /**
1773  * nm_mbox_check_stats - Check the Mailbox statistics - Implements MxOps::mbox_check_stats() - @ingroup mx_mbox_check_stats
1774  */
nm_mbox_check_stats(struct Mailbox * m,uint8_t flags)1775 static enum MxStatus nm_mbox_check_stats(struct Mailbox *m, uint8_t flags)
1776 {
1777   struct UrlQuery *item = NULL;
1778   struct Url *url = NULL;
1779   const char *db_filename = NULL;
1780   char *db_query = NULL;
1781   notmuch_database_t *db = NULL;
1782   enum MxStatus rc = MX_STATUS_ERROR;
1783   const short c_nm_db_limit = cs_subset_number(NeoMutt->sub, "nm_db_limit");
1784   int limit = c_nm_db_limit;
1785   mutt_debug(LL_DEBUG1, "nm: count\n");
1786 
1787   url = url_parse(mailbox_path(m));
1788   if (!url)
1789   {
1790     mutt_error(_("failed to parse notmuch url: %s"), mailbox_path(m));
1791     goto done;
1792   }
1793 
1794   STAILQ_FOREACH(item, &url->query_strings, entries)
1795   {
1796     if (item->value && (strcmp(item->name, "query") == 0))
1797       db_query = item->value;
1798     else if (item->value && (strcmp(item->name, "limit") == 0))
1799     {
1800       // Try to parse the limit
1801       if (mutt_str_atoi(item->value, &limit) != 0)
1802       {
1803         mutt_error(_("failed to parse limit: %s"), item->value);
1804         goto done;
1805       }
1806     }
1807   }
1808 
1809   if (!db_query)
1810     goto done;
1811 
1812   db_filename = url->path;
1813   if (!db_filename)
1814     db_filename = nm_db_get_filename(m);
1815 
1816   /* don't be verbose about connection, as we're called from
1817    * sidebar/mailbox very often */
1818   db = nm_db_do_open(db_filename, false, false);
1819   if (!db)
1820     goto done;
1821 
1822   /* all emails */
1823   m->msg_count = count_query(db, db_query, limit);
1824   while (m->email_max < m->msg_count)
1825     mx_alloc_memory(m);
1826 
1827   // holder variable for extending query to unread/flagged
1828   char *qstr = NULL;
1829 
1830   // unread messages
1831   const char *const c_nm_unread_tag =
1832       cs_subset_string(NeoMutt->sub, "nm_unread_tag");
1833   mutt_str_asprintf(&qstr, "( %s ) tag:%s", db_query, c_nm_unread_tag);
1834   m->msg_unread = count_query(db, qstr, limit);
1835   FREE(&qstr);
1836 
1837   // flagged messages
1838   const char *const c_nm_flagged_tag =
1839       cs_subset_string(NeoMutt->sub, "nm_flagged_tag");
1840   mutt_str_asprintf(&qstr, "( %s ) tag:%s", db_query, c_nm_flagged_tag);
1841   m->msg_flagged = count_query(db, qstr, limit);
1842   FREE(&qstr);
1843 
1844   rc = (m->msg_new > 0) ? MX_STATUS_NEW_MAIL : MX_STATUS_OK;
1845 done:
1846   if (db)
1847   {
1848     nm_db_free(db);
1849     mutt_debug(LL_DEBUG1, "nm: count close DB\n");
1850   }
1851   url_free(&url);
1852 
1853   mutt_debug(LL_DEBUG1, "nm: count done [rc=%d]\n", rc);
1854   return rc;
1855 }
1856 
1857 /**
1858  * get_default_mailbox - Get Mailbox for notmuch without any parameters.
1859  * @retval ptr Mailbox pointer.
1860  */
get_default_mailbox(void)1861 static struct Mailbox *get_default_mailbox(void)
1862 {
1863   // Create a new notmuch mailbox from scratch and add plumbing for DB access.
1864   char *default_url = nm_get_default_url();
1865   struct Mailbox *m = mx_path_resolve(default_url);
1866 
1867   FREE(&default_url);
1868 
1869   // These are no-ops for an initialized mailbox.
1870   init_mailbox(m);
1871   mx_mbox_ac_link(m);
1872 
1873   return m;
1874 }
1875 
1876 /**
1877  * nm_record_message - Add a message to the Notmuch database
1878  * @param m    Mailbox
1879  * @param path Path of the email
1880  * @param e    Email
1881  * @retval  0 Success
1882  * @retval -1 Failure
1883  */
nm_record_message(struct Mailbox * m,char * path,struct Email * e)1884 int nm_record_message(struct Mailbox *m, char *path, struct Email *e)
1885 {
1886   notmuch_database_t *db = NULL;
1887   notmuch_status_t st;
1888   notmuch_message_t *msg = NULL;
1889   int rc = -1;
1890 
1891   struct NmMboxData *mdata = nm_mdata_get(m);
1892 
1893   // If no notmuch data, fall back to the default mailbox.
1894   //
1895   // IMPORTANT: DO NOT FREE THIS MAILBOX. Two reasons:
1896   // 1) If user has default mailbox in config, we'll be removing it. That's not
1897   //    good program behavior!
1898   // 2) If not in user's config, keep mailbox around for future nm_record calls.
1899   //    It saves NeoMutt from allocating/deallocating repeatedly.
1900   if (!mdata)
1901   {
1902     mutt_debug(LL_DEBUG1, "nm: non-nm mailbox. trying the default nm mailbox.");
1903     m = get_default_mailbox();
1904     mdata = nm_mdata_get(m);
1905   }
1906 
1907   if (!path || !mdata || (access(path, F_OK) != 0))
1908     return 0;
1909   db = nm_db_get(m, true);
1910   if (!db)
1911     return -1;
1912 
1913   mutt_debug(LL_DEBUG1, "nm: record message: %s\n", path);
1914   int trans = nm_db_trans_begin(m);
1915   if (trans < 0)
1916     goto done;
1917 
1918 #if LIBNOTMUCH_CHECK_VERSION(5, 1, 0)
1919   st = notmuch_database_index_file(db, path, NULL, &msg);
1920 #else
1921   st = notmuch_database_add_message(db, path, &msg);
1922 #endif
1923 
1924   if ((st != NOTMUCH_STATUS_SUCCESS) && (st != NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID))
1925   {
1926     mutt_debug(LL_DEBUG1, "nm: failed to add '%s' [st=%d]\n", path, (int) st);
1927     goto done;
1928   }
1929 
1930   if ((st == NOTMUCH_STATUS_SUCCESS) && msg)
1931   {
1932     notmuch_message_maildir_flags_to_tags(msg);
1933     if (e)
1934     {
1935       char *tags = driver_tags_get(&e->tags);
1936       update_tags(msg, tags);
1937       FREE(&tags);
1938     }
1939     const char *const c_nm_record_tags =
1940         cs_subset_string(NeoMutt->sub, "nm_record_tags");
1941     if (c_nm_record_tags)
1942       update_tags(msg, c_nm_record_tags);
1943   }
1944 
1945   rc = 0;
1946 done:
1947   if (msg)
1948     notmuch_message_destroy(msg);
1949   if (trans == 1)
1950     nm_db_trans_end(m);
1951 
1952   nm_db_release(m);
1953 
1954   return rc;
1955 }
1956 
1957 /**
1958  * nm_get_all_tags - Fill a list with all notmuch tags
1959  * @param[in]  m         Mailbox
1960  * @param[out] tag_list  List of tags
1961  * @param[out] tag_count Number of tags
1962  * @retval  0 Success
1963  * @retval -1 Failure
1964  *
1965  * If tag_list is NULL, just count the tags.
1966  */
nm_get_all_tags(struct Mailbox * m,char ** tag_list,int * tag_count)1967 int nm_get_all_tags(struct Mailbox *m, char **tag_list, int *tag_count)
1968 {
1969   struct NmMboxData *mdata = nm_mdata_get(m);
1970   if (!mdata)
1971     return -1;
1972 
1973   notmuch_database_t *db = NULL;
1974   notmuch_tags_t *tags = NULL;
1975   const char *tag = NULL;
1976   int rc = -1;
1977 
1978   if (!(db = nm_db_get(m, false)) || !(tags = notmuch_database_get_all_tags(db)))
1979     goto done;
1980 
1981   *tag_count = 0;
1982   mutt_debug(LL_DEBUG1, "nm: get all tags\n");
1983 
1984   while (notmuch_tags_valid(tags))
1985   {
1986     tag = notmuch_tags_get(tags);
1987     /* Skip empty string */
1988     if (*tag)
1989     {
1990       if (tag_list)
1991         tag_list[*tag_count] = mutt_str_dup(tag);
1992       (*tag_count)++;
1993     }
1994     notmuch_tags_move_to_next(tags);
1995   }
1996 
1997   rc = 0;
1998 done:
1999   if (tags)
2000     notmuch_tags_destroy(tags);
2001 
2002   nm_db_release(m);
2003 
2004   mutt_debug(LL_DEBUG1, "nm: get all tags done [rc=%d tag_count=%u]\n", rc, *tag_count);
2005   return rc;
2006 }
2007 
2008 /**
2009  * nm_ac_owns_path - Check whether an Account owns a Mailbox path - Implements MxOps::ac_owns_path() - @ingroup mx_ac_owns_path
2010  */
nm_ac_owns_path(struct Account * a,const char * path)2011 static bool nm_ac_owns_path(struct Account *a, const char *path)
2012 {
2013   return true;
2014 }
2015 
2016 /**
2017  * nm_ac_add - Add a Mailbox to an Account - Implements MxOps::ac_add() - @ingroup mx_ac_add
2018  */
nm_ac_add(struct Account * a,struct Mailbox * m)2019 static bool nm_ac_add(struct Account *a, struct Mailbox *m)
2020 {
2021   if (a->adata)
2022     return true;
2023 
2024   struct NmAccountData *adata = nm_adata_new();
2025   a->adata = adata;
2026   a->adata_free = nm_adata_free;
2027 
2028   return true;
2029 }
2030 
2031 /**
2032  * nm_mbox_open - Open a Mailbox - Implements MxOps::mbox_open() - @ingroup mx_mbox_open
2033  */
nm_mbox_open(struct Mailbox * m)2034 static enum MxOpenReturns nm_mbox_open(struct Mailbox *m)
2035 {
2036   if (init_mailbox(m) != 0)
2037     return MX_OPEN_ERROR;
2038 
2039   struct NmMboxData *mdata = nm_mdata_get(m);
2040   if (!mdata)
2041     return MX_OPEN_ERROR;
2042 
2043   mutt_debug(LL_DEBUG1, "nm: reading messages...[current count=%d]\n", m->msg_count);
2044 
2045   progress_setup(m);
2046   enum MxOpenReturns rc = MX_OPEN_ERROR;
2047 
2048   notmuch_query_t *q = get_query(m, false);
2049   if (q)
2050   {
2051     rc = MX_OPEN_OK;
2052     switch (mdata->query_type)
2053     {
2054       case NM_QUERY_TYPE_UNKNOWN: // UNKNOWN should never occur, but MESGS is default
2055       case NM_QUERY_TYPE_MESGS:
2056         if (!read_mesgs_query(m, q, false))
2057           rc = MX_OPEN_ABORT;
2058         break;
2059       case NM_QUERY_TYPE_THREADS:
2060         if (!read_threads_query(m, q, false, get_limit(mdata)))
2061           rc = MX_OPEN_ABORT;
2062         break;
2063     }
2064     notmuch_query_destroy(q);
2065   }
2066 
2067   nm_db_release(m);
2068 
2069   m->mtime.tv_sec = mutt_date_epoch();
2070   m->mtime.tv_nsec = 0;
2071 
2072   mdata->oldmsgcount = 0;
2073 
2074   mutt_debug(LL_DEBUG1, "nm: reading messages... done [rc=%d, count=%d]\n", rc, m->msg_count);
2075   progress_free(&mdata->progress);
2076   return rc;
2077 }
2078 
2079 /**
2080  * nm_mbox_check - Check for new mail - Implements MxOps::mbox_check() - @ingroup mx_mbox_check
2081  * @param m Mailbox
2082  * @retval enum #MxStatus
2083  */
nm_mbox_check(struct Mailbox * m)2084 static enum MxStatus nm_mbox_check(struct Mailbox *m)
2085 {
2086   struct NmMboxData *mdata = nm_mdata_get(m);
2087   time_t mtime = 0;
2088   if (!mdata || (nm_db_get_mtime(m, &mtime) != 0))
2089     return MX_STATUS_ERROR;
2090 
2091   int new_flags = 0;
2092   bool occult = false;
2093 
2094   if (m->mtime.tv_sec >= mtime)
2095   {
2096     mutt_debug(LL_DEBUG2, "nm: check unnecessary (db=%lu mailbox=%lu)\n", mtime,
2097                m->mtime.tv_sec);
2098     return MX_STATUS_OK;
2099   }
2100 
2101   mutt_debug(LL_DEBUG1, "nm: checking (db=%lu mailbox=%lu)\n", mtime, m->mtime.tv_sec);
2102 
2103   notmuch_query_t *q = get_query(m, false);
2104   if (!q)
2105     goto done;
2106 
2107   mutt_debug(LL_DEBUG1, "nm: start checking (count=%d)\n", m->msg_count);
2108   mdata->oldmsgcount = m->msg_count;
2109 
2110   for (int i = 0; i < m->msg_count; i++)
2111   {
2112     struct Email *e = m->emails[i];
2113     if (!e)
2114       break;
2115 
2116     e->active = false;
2117   }
2118 
2119   int limit = get_limit(mdata);
2120 
2121   notmuch_messages_t *msgs = get_messages(q);
2122 
2123   // TODO: Analyze impact of removing this version guard.
2124 #if LIBNOTMUCH_CHECK_VERSION(5, 0, 0)
2125   if (!msgs)
2126     return MX_STATUS_OK;
2127 #elif LIBNOTMUCH_CHECK_VERSION(4, 3, 0)
2128   if (!msgs)
2129     goto done;
2130 #endif
2131 
2132   struct HeaderCache *h = nm_hcache_open(m);
2133 
2134   for (int i = 0; notmuch_messages_valid(msgs) && ((limit == 0) || (i < limit));
2135        notmuch_messages_move_to_next(msgs), i++)
2136   {
2137     notmuch_message_t *msg = notmuch_messages_get(msgs);
2138     struct Email *e = get_mutt_email(m, msg);
2139 
2140     if (!e)
2141     {
2142       /* new email */
2143       append_message(h, m, msg, false);
2144       notmuch_message_destroy(msg);
2145       continue;
2146     }
2147 
2148     /* message already exists, merge flags */
2149     e->active = true;
2150 
2151     /* Check to see if the message has moved to a different subdirectory.
2152      * If so, update the associated filename.  */
2153     const char *new_file = get_message_last_filename(msg);
2154     char old_file[PATH_MAX];
2155     email_get_fullpath(e, old_file, sizeof(old_file));
2156 
2157     if (!mutt_str_equal(old_file, new_file))
2158       update_message_path(e, new_file);
2159 
2160     if (!e->changed)
2161     {
2162       /* if the user hasn't modified the flags on this message, update the
2163        * flags we just detected.  */
2164       struct Email e_tmp = { 0 };
2165       e_tmp.edata = maildir_edata_new();
2166       maildir_parse_flags(&e_tmp, new_file);
2167       maildir_update_flags(m, e, &e_tmp);
2168       maildir_edata_free(&e_tmp.edata);
2169     }
2170 
2171     if (update_email_tags(e, msg) == 0)
2172       new_flags++;
2173 
2174     notmuch_message_destroy(msg);
2175   }
2176 
2177   nm_hcache_close(h);
2178 
2179   for (int i = 0; i < m->msg_count; i++)
2180   {
2181     struct Email *e = m->emails[i];
2182     if (!e)
2183       break;
2184 
2185     if (!e->active)
2186     {
2187       occult = true;
2188       break;
2189     }
2190   }
2191 
2192   if (m->msg_count > mdata->oldmsgcount)
2193     mailbox_changed(m, NT_MAILBOX_INVALID);
2194 done:
2195   if (q)
2196     notmuch_query_destroy(q);
2197 
2198   nm_db_release(m);
2199 
2200   m->mtime.tv_sec = mutt_date_epoch();
2201   m->mtime.tv_nsec = 0;
2202 
2203   mutt_debug(LL_DEBUG1, "nm: ... check done [count=%d, new_flags=%d, occult=%d]\n",
2204              m->msg_count, new_flags, occult);
2205 
2206   if (occult)
2207     return MX_STATUS_REOPENED;
2208   if (m->msg_count > mdata->oldmsgcount)
2209     return MX_STATUS_NEW_MAIL;
2210   if (new_flags)
2211     return MX_STATUS_FLAGS;
2212   return MX_STATUS_OK;
2213 }
2214 
2215 /**
2216  * nm_mbox_sync - Save changes to the Mailbox - Implements MxOps::mbox_sync() - @ingroup mx_mbox_sync
2217  */
nm_mbox_sync(struct Mailbox * m)2218 static enum MxStatus nm_mbox_sync(struct Mailbox *m)
2219 {
2220   struct NmMboxData *mdata = nm_mdata_get(m);
2221   if (!mdata)
2222     return MX_STATUS_ERROR;
2223 
2224   enum MxStatus rc = MX_STATUS_OK;
2225   struct Progress *progress = NULL;
2226   char *url = mutt_str_dup(mailbox_path(m));
2227   bool changed = false;
2228 
2229   mutt_debug(LL_DEBUG1, "nm: sync start\n");
2230 
2231   if (m->verbose)
2232   {
2233     /* all is in this function so we don't use data->progress here */
2234     char msg[PATH_MAX];
2235     snprintf(msg, sizeof(msg), _("Writing %s..."), mailbox_path(m));
2236     progress = progress_new(msg, MUTT_PROGRESS_WRITE, m->msg_count);
2237   }
2238 
2239   struct HeaderCache *h = nm_hcache_open(m);
2240 
2241   int mh_sync_errors = 0;
2242   for (int i = 0; i < m->msg_count; i++)
2243   {
2244     char old_file[PATH_MAX], new_file[PATH_MAX];
2245     struct Email *e = m->emails[i];
2246     if (!e)
2247       break;
2248 
2249     struct NmEmailData *edata = nm_edata_get(e);
2250 
2251     if (m->verbose)
2252       progress_update(progress, i, -1);
2253 
2254     *old_file = '\0';
2255     *new_file = '\0';
2256 
2257     if (edata->oldpath)
2258     {
2259       mutt_str_copy(old_file, edata->oldpath, sizeof(old_file));
2260       old_file[sizeof(old_file) - 1] = '\0';
2261       mutt_debug(LL_DEBUG2, "nm: fixing obsolete path '%s'\n", old_file);
2262     }
2263     else
2264       email_get_fullpath(e, old_file, sizeof(old_file));
2265 
2266     mutt_buffer_strcpy(&m->pathbuf, edata->folder);
2267     m->type = edata->type;
2268 
2269     bool ok = maildir_sync_mailbox_message(m, i, h);
2270     if (!ok)
2271     {
2272       // Syncing file failed, query notmuch for new filepath.
2273       m->type = MUTT_NOTMUCH;
2274       notmuch_database_t *db = nm_db_get(m, true);
2275       if (db)
2276       {
2277         notmuch_message_t *msg = get_nm_message(db, e);
2278 
2279         sync_email_path_with_nm(e, msg);
2280 
2281         mutt_buffer_strcpy(&m->pathbuf, edata->folder);
2282         m->type = edata->type;
2283         ok = maildir_sync_mailbox_message(m, i, h);
2284         m->type = MUTT_NOTMUCH;
2285       }
2286       nm_db_release(m);
2287       m->type = edata->type;
2288     }
2289 
2290     mutt_buffer_strcpy(&m->pathbuf, url);
2291     m->type = MUTT_NOTMUCH;
2292 
2293     if (!ok)
2294     {
2295       mh_sync_errors += 1;
2296       continue;
2297     }
2298 
2299     if (!e->deleted)
2300       email_get_fullpath(e, new_file, sizeof(new_file));
2301 
2302     if (e->deleted || (strcmp(old_file, new_file) != 0))
2303     {
2304       if (e->deleted && (remove_filename(m, old_file) == 0))
2305         changed = true;
2306       else if (*new_file && *old_file && (rename_filename(m, old_file, new_file, e) == 0))
2307         changed = true;
2308     }
2309 
2310     FREE(&edata->oldpath);
2311   }
2312 
2313   if (mh_sync_errors > 0)
2314   {
2315     mutt_error(
2316         ngettext(
2317             "Unable to sync %d message due to external mailbox modification",
2318             "Unable to sync %d messages due to external mailbox modification", mh_sync_errors),
2319         mh_sync_errors);
2320   }
2321 
2322   mutt_buffer_strcpy(&m->pathbuf, url);
2323   m->type = MUTT_NOTMUCH;
2324 
2325   nm_db_release(m);
2326 
2327   if (changed)
2328   {
2329     m->mtime.tv_sec = mutt_date_epoch();
2330     m->mtime.tv_nsec = 0;
2331   }
2332 
2333   nm_hcache_close(h);
2334 
2335   progress_free(&progress);
2336   FREE(&url);
2337   mutt_debug(LL_DEBUG1, "nm: .... sync done [rc=%d]\n", rc);
2338   return rc;
2339 }
2340 
2341 /**
2342  * nm_mbox_close - Close a Mailbox - Implements MxOps::mbox_close() - @ingroup mx_mbox_close
2343  *
2344  * Nothing to do.
2345  */
nm_mbox_close(struct Mailbox * m)2346 static enum MxStatus nm_mbox_close(struct Mailbox *m)
2347 {
2348   return MX_STATUS_OK;
2349 }
2350 
2351 /**
2352  * nm_msg_open - Open an email message in a Mailbox - Implements MxOps::msg_open() - @ingroup mx_msg_open
2353  */
nm_msg_open(struct Mailbox * m,struct Message * msg,int msgno)2354 static bool nm_msg_open(struct Mailbox *m, struct Message *msg, int msgno)
2355 {
2356   struct Email *e = m->emails[msgno];
2357   if (!e)
2358     return false;
2359 
2360   char path[PATH_MAX];
2361   char *folder = nm_email_get_folder(e);
2362 
2363   snprintf(path, sizeof(path), "%s/%s", folder, e->path);
2364 
2365   msg->fp = fopen(path, "r");
2366   if (!msg->fp && (errno == ENOENT) && ((m->type == MUTT_MAILDIR) || (m->type == MUTT_NOTMUCH)))
2367   {
2368     msg->fp = maildir_open_find_message(folder, e->path, NULL);
2369   }
2370 
2371   return msg->fp != NULL;
2372 }
2373 
2374 /**
2375  * nm_msg_commit - Save changes to an email - Implements MxOps::msg_commit() - @ingroup mx_msg_commit
2376  * @retval -1 Always
2377  */
nm_msg_commit(struct Mailbox * m,struct Message * msg)2378 static int nm_msg_commit(struct Mailbox *m, struct Message *msg)
2379 {
2380   mutt_error(_("Can't write to virtual folder"));
2381   return -1;
2382 }
2383 
2384 /**
2385  * nm_msg_close - Close an email - Implements MxOps::msg_close() - @ingroup mx_msg_close
2386  */
nm_msg_close(struct Mailbox * m,struct Message * msg)2387 static int nm_msg_close(struct Mailbox *m, struct Message *msg)
2388 {
2389   mutt_file_fclose(&(msg->fp));
2390   return 0;
2391 }
2392 
2393 /**
2394  * nm_tags_edit - Prompt and validate new messages tags - Implements MxOps::tags_edit() - @ingroup mx_tags_edit
2395  */
nm_tags_edit(struct Mailbox * m,const char * tags,char * buf,size_t buflen)2396 static int nm_tags_edit(struct Mailbox *m, const char *tags, char *buf, size_t buflen)
2397 {
2398   *buf = '\0';
2399   if (mutt_get_field("Add/remove labels: ", buf, buflen, MUTT_NM_TAG, false, NULL, NULL) != 0)
2400   {
2401     return -1;
2402   }
2403   return 1;
2404 }
2405 
2406 /**
2407  * nm_tags_commit - Save the tags to a message - Implements MxOps::tags_commit() - @ingroup mx_tags_commit
2408  */
nm_tags_commit(struct Mailbox * m,struct Email * e,char * buf)2409 static int nm_tags_commit(struct Mailbox *m, struct Email *e, char *buf)
2410 {
2411   if (*buf == '\0')
2412     return 0; /* no tag change, so nothing to do */
2413 
2414   struct NmMboxData *mdata = nm_mdata_get(m);
2415   if (!mdata)
2416     return -1;
2417 
2418   notmuch_database_t *db = NULL;
2419   notmuch_message_t *msg = NULL;
2420   int rc = -1;
2421 
2422   if (!(db = nm_db_get(m, true)) || !(msg = get_nm_message(db, e)))
2423     goto done;
2424 
2425   mutt_debug(LL_DEBUG1, "nm: tags modify: '%s'\n", buf);
2426 
2427   update_tags(msg, buf);
2428   update_email_flags(m, e, buf);
2429   update_email_tags(e, msg);
2430   mutt_set_header_color(m, e);
2431 
2432   rc = 0;
2433   e->changed = true;
2434 done:
2435   nm_db_release(m);
2436   if (e->changed)
2437   {
2438     m->mtime.tv_sec = mutt_date_epoch();
2439     m->mtime.tv_nsec = 0;
2440   }
2441   mutt_debug(LL_DEBUG1, "nm: tags modify done [rc=%d]\n", rc);
2442   return rc;
2443 }
2444 
2445 /**
2446  * nm_path_probe - Is this a Notmuch Mailbox? - Implements MxOps::path_probe() - @ingroup mx_path_probe
2447  */
nm_path_probe(const char * path,const struct stat * st)2448 enum MailboxType nm_path_probe(const char *path, const struct stat *st)
2449 {
2450   if (!mutt_istr_startswith(path, NmUrlProtocol))
2451     return MUTT_UNKNOWN;
2452 
2453   return MUTT_NOTMUCH;
2454 }
2455 
2456 /**
2457  * nm_path_canon - Canonicalise a Mailbox path - Implements MxOps::path_canon() - @ingroup mx_path_canon
2458  */
nm_path_canon(char * buf,size_t buflen)2459 static int nm_path_canon(char *buf, size_t buflen)
2460 {
2461   return 0;
2462 }
2463 
2464 /**
2465  * nm_path_pretty - Abbreviate a Mailbox path - Implements MxOps::path_pretty() - @ingroup mx_path_pretty
2466  */
nm_path_pretty(char * buf,size_t buflen,const char * folder)2467 static int nm_path_pretty(char *buf, size_t buflen, const char *folder)
2468 {
2469   /* Succeed, but don't do anything, for now */
2470   return 0;
2471 }
2472 
2473 /**
2474  * nm_path_parent - Find the parent of a Mailbox path - Implements MxOps::path_parent() - @ingroup mx_path_parent
2475  */
nm_path_parent(char * buf,size_t buflen)2476 static int nm_path_parent(char *buf, size_t buflen)
2477 {
2478   /* Succeed, but don't do anything, for now */
2479   return 0;
2480 }
2481 
2482 /**
2483  * MxNotmuchOps - Notmuch Mailbox - Implements ::MxOps - @ingroup mx_api
2484  */
2485 struct MxOps MxNotmuchOps = {
2486   // clang-format off
2487   .type            = MUTT_NOTMUCH,
2488   .name             = "notmuch",
2489   .is_local         = false,
2490   .ac_owns_path     = nm_ac_owns_path,
2491   .ac_add           = nm_ac_add,
2492   .mbox_open        = nm_mbox_open,
2493   .mbox_open_append = NULL,
2494   .mbox_check       = nm_mbox_check,
2495   .mbox_check_stats = nm_mbox_check_stats,
2496   .mbox_sync        = nm_mbox_sync,
2497   .mbox_close       = nm_mbox_close,
2498   .msg_open         = nm_msg_open,
2499   .msg_open_new     = maildir_msg_open_new,
2500   .msg_commit       = nm_msg_commit,
2501   .msg_close        = nm_msg_close,
2502   .msg_padding_size = NULL,
2503   .msg_save_hcache  = NULL,
2504   .tags_edit        = nm_tags_edit,
2505   .tags_commit      = nm_tags_commit,
2506   .path_probe       = nm_path_probe,
2507   .path_canon       = nm_path_canon,
2508   .path_pretty      = nm_path_pretty,
2509   .path_parent      = nm_path_parent,
2510   .path_is_empty    = NULL,
2511   // clang-format on
2512 };
2513