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