1 /**
2 * @file
3 * Match patterns to emails
4 *
5 * @authors
6 * Copyright (C) 1996-2000,2006-2007,2010 Michael R. Elkins <me@mutt.org>
7 * Copyright (C) 2019 Pietro Cerutti <gahr@gahr.ch>
8 * Copyright (C) 2020 R Primus <rprimus@gmail.com>
9 *
10 * @copyright
11 * This program is free software: you can redistribute it and/or modify it under
12 * the terms of the GNU General Public License as published by the Free Software
13 * Foundation, either version 2 of the License, or (at your option) any later
14 * version.
15 *
16 * This program is distributed in the hope that it will be useful, but WITHOUT
17 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
19 * details.
20 *
21 * You should have received a copy of the GNU General Public License along with
22 * this program. If not, see <http://www.gnu.org/licenses/>.
23 */
24
25 /**
26 * @page pattern_pattern Match patterns to emails
27 *
28 * Match patterns to emails
29 */
30
31 #include "config.h"
32 #include <stddef.h>
33 #include <stdbool.h>
34 #include "private.h"
35 #include "mutt/lib.h"
36 #include "config/lib.h"
37 #include "email/lib.h"
38 #include "core/lib.h"
39 #include "alias/gui.h" // IWYU pragma: keep
40 #include "alias/lib.h"
41 #include "gui/lib.h"
42 #include "mutt.h"
43 #include "lib.h"
44 #include "menu/lib.h"
45 #include "progress/lib.h"
46 #include "context.h"
47 #include "mutt_globals.h"
48 #include "mutt_logging.h"
49 #include "mx.h"
50 #include "opcodes.h"
51 #include "options.h"
52 #include "protos.h"
53 #ifndef USE_FMEMOPEN
54 #include <sys/stat.h>
55 #endif
56 #ifdef USE_IMAP
57 #include "imap/lib.h"
58 #endif
59
60 /**
61 * RangeRegexes - Set of Regexes for various range types
62 *
63 * This array, will also contain the compiled regexes.
64 */
65 struct RangeRegex RangeRegexes[] = {
66 // clang-format off
67 [RANGE_K_REL] = { RANGE_REL_RX, 1, 3, 0, { 0 } },
68 [RANGE_K_ABS] = { RANGE_ABS_RX, 1, 3, 0, { 0 } },
69 [RANGE_K_LT] = { RANGE_LT_RX, 1, 2, 0, { 0 } },
70 [RANGE_K_GT] = { RANGE_GT_RX, 2, 1, 0, { 0 } },
71 [RANGE_K_BARE] = { RANGE_BARE_RX, 1, 1, 0, { 0 } },
72 // clang-format on
73 };
74
75 /**
76 * @defgroup eat_arg_api Parse a pattern
77 *
78 * Prototype for a function to parse a pattern
79 *
80 * @param pat Pattern to store the results in
81 * @param flags Flags, e.g. #MUTT_PC_PATTERN_DYNAMIC
82 * @param s String to parse
83 * @param err Buffer for error messages
84 * @retval true The pattern was read successfully
85 */
86 typedef bool (*eat_arg_t)(struct Pattern *pat, PatternCompFlags flags,
87 struct Buffer *s, struct Buffer *err);
88
89 static struct PatternList *SearchPattern = NULL; ///< current search pattern
90 static char LastSearch[256] = { 0 }; ///< last pattern searched for
91 static char LastSearchExpn[1024] = { 0 }; ///< expanded version of LastSearch
92
93 /**
94 * quote_simple - Apply simple quoting to a string
95 * @param str String to quote
96 * @param buf Buffer for the result
97 */
quote_simple(const char * str,struct Buffer * buf)98 static void quote_simple(const char *str, struct Buffer *buf)
99 {
100 mutt_buffer_reset(buf);
101 mutt_buffer_addch(buf, '"');
102 while (*str)
103 {
104 if ((*str == '\\') || (*str == '"'))
105 mutt_buffer_addch(buf, '\\');
106 mutt_buffer_addch(buf, *str++);
107 }
108 mutt_buffer_addch(buf, '"');
109 }
110
111 /**
112 * mutt_check_simple - Convert a simple search into a real request
113 * @param buf Buffer for the result
114 * @param simple Search string to convert
115 */
mutt_check_simple(struct Buffer * buf,const char * simple)116 void mutt_check_simple(struct Buffer *buf, const char *simple)
117 {
118 bool do_simple = true;
119
120 for (const char *p = mutt_buffer_string(buf); p && (p[0] != '\0'); p++)
121 {
122 if ((p[0] == '\\') && (p[1] != '\0'))
123 p++;
124 else if ((p[0] == '~') || (p[0] == '=') || (p[0] == '%'))
125 {
126 do_simple = false;
127 break;
128 }
129 }
130
131 /* XXX - is mutt_istr_cmp() right here, or should we use locale's
132 * equivalences? */
133
134 if (do_simple) /* yup, so spoof a real request */
135 {
136 /* convert old tokens into the new format */
137 if (mutt_istr_equal("all", mutt_buffer_string(buf)) ||
138 mutt_str_equal("^", mutt_buffer_string(buf)) ||
139 mutt_str_equal(".", mutt_buffer_string(buf))) /* ~A is more efficient */
140 {
141 mutt_buffer_strcpy(buf, "~A");
142 }
143 else if (mutt_istr_equal("del", mutt_buffer_string(buf)))
144 mutt_buffer_strcpy(buf, "~D");
145 else if (mutt_istr_equal("flag", mutt_buffer_string(buf)))
146 mutt_buffer_strcpy(buf, "~F");
147 else if (mutt_istr_equal("new", mutt_buffer_string(buf)))
148 mutt_buffer_strcpy(buf, "~N");
149 else if (mutt_istr_equal("old", mutt_buffer_string(buf)))
150 mutt_buffer_strcpy(buf, "~O");
151 else if (mutt_istr_equal("repl", mutt_buffer_string(buf)))
152 mutt_buffer_strcpy(buf, "~Q");
153 else if (mutt_istr_equal("read", mutt_buffer_string(buf)))
154 mutt_buffer_strcpy(buf, "~R");
155 else if (mutt_istr_equal("tag", mutt_buffer_string(buf)))
156 mutt_buffer_strcpy(buf, "~T");
157 else if (mutt_istr_equal("unread", mutt_buffer_string(buf)))
158 mutt_buffer_strcpy(buf, "~U");
159 else
160 {
161 struct Buffer *tmp = mutt_buffer_pool_get();
162 quote_simple(mutt_buffer_string(buf), tmp);
163 mutt_file_expand_fmt(buf, simple, mutt_buffer_string(tmp));
164 mutt_buffer_pool_release(&tmp);
165 }
166 }
167 }
168
169 /**
170 * top_of_thread - Find the first email in the current thread
171 * @param e Current Email
172 * @retval ptr Success, email found
173 * @retval NULL Error
174 */
top_of_thread(struct Email * e)175 static struct MuttThread *top_of_thread(struct Email *e)
176 {
177 if (!e)
178 return NULL;
179
180 struct MuttThread *t = e->thread;
181
182 while (t && t->parent)
183 t = t->parent;
184
185 return t;
186 }
187
188 /**
189 * mutt_limit_current_thread - Limit the email view to the current thread
190 * @param ctx Current Mailbox
191 * @param e Current Email
192 * @retval true Success
193 * @retval false Failure
194 */
mutt_limit_current_thread(struct Context * ctx,struct Email * e)195 bool mutt_limit_current_thread(struct Context *ctx, struct Email *e)
196 {
197 if (!ctx || !ctx->mailbox || !e)
198 return false;
199
200 struct Mailbox *m = ctx->mailbox;
201
202 struct MuttThread *me = top_of_thread(e);
203 if (!me)
204 return false;
205
206 m->vcount = 0;
207 ctx->vsize = 0;
208 ctx->collapsed = false;
209
210 for (int i = 0; i < m->msg_count; i++)
211 {
212 e = m->emails[i];
213 if (!e)
214 break;
215
216 e->vnum = -1;
217 e->visible = false;
218 e->collapsed = false;
219 e->num_hidden = 0;
220
221 if (top_of_thread(e) == me)
222 {
223 struct Body *body = e->body;
224
225 e->vnum = m->vcount;
226 e->visible = true;
227 m->v2r[m->vcount] = i;
228 m->vcount++;
229 ctx->vsize += (body->length + body->offset - body->hdr_offset);
230 }
231 }
232 return true;
233 }
234
235 /**
236 * mutt_pattern_alias_func - Perform some Pattern matching for Alias
237 * @param prompt Prompt to show the user
238 * @param mdata Menu data holding Aliases
239 * @param menu Current menu
240 * @retval 0 Success
241 * @retval -1 Failure
242 */
mutt_pattern_alias_func(char * prompt,struct AliasMenuData * mdata,struct Menu * menu)243 int mutt_pattern_alias_func(char *prompt, struct AliasMenuData *mdata, struct Menu *menu)
244 {
245 int rc = -1;
246 struct Progress *progress = NULL;
247 struct Buffer *buf = mutt_buffer_pool_get();
248
249 mutt_buffer_strcpy(buf, mdata->str);
250 if (prompt)
251 {
252 if ((mutt_buffer_get_field(prompt, buf, MUTT_PATTERN | MUTT_CLEAR, false,
253 NULL, NULL, NULL) != 0) ||
254 mutt_buffer_is_empty(buf))
255 {
256 mutt_buffer_pool_release(&buf);
257 return -1;
258 }
259 }
260
261 mutt_message(_("Compiling search pattern..."));
262
263 bool match_all = false;
264 struct PatternList *pat = NULL;
265 char *simple = mutt_buffer_strdup(buf);
266 if (simple)
267 {
268 mutt_check_simple(buf, MUTT_ALIAS_SIMPLESEARCH);
269 const char *pbuf = buf->data;
270 while (*pbuf == ' ')
271 pbuf++;
272 match_all = mutt_str_equal(pbuf, "~A");
273
274 struct Buffer err = mutt_buffer_make(0);
275 pat = mutt_pattern_comp(NULL, menu, buf->data, MUTT_PC_FULL_MSG, &err);
276 if (!pat)
277 {
278 mutt_error("%s", mutt_buffer_string(&err));
279 mutt_buffer_dealloc(&err);
280 goto bail;
281 }
282 }
283 else
284 {
285 match_all = true;
286 }
287
288 progress = progress_new(_("Executing command on matching messages..."),
289 MUTT_PROGRESS_READ, ARRAY_SIZE(&mdata->ava));
290
291 int vcounter = 0;
292 struct AliasView *avp = NULL;
293 ARRAY_FOREACH(avp, &mdata->ava)
294 {
295 progress_update(progress, ARRAY_FOREACH_IDX, -1);
296
297 if (match_all ||
298 mutt_pattern_alias_exec(SLIST_FIRST(pat), MUTT_MATCH_FULL_ADDRESS, avp, NULL))
299 {
300 avp->is_visible = true;
301 vcounter++;
302 }
303 else
304 {
305 avp->is_visible = false;
306 }
307 }
308 progress_free(&progress);
309
310 FREE(&mdata->str);
311 if (!match_all)
312 {
313 mdata->str = simple;
314 simple = NULL;
315 }
316
317 if (menu)
318 {
319 menu->max = vcounter;
320 menu_set_index(menu, 0);
321 }
322
323 mutt_clear_error();
324
325 rc = 0;
326
327 bail:
328 mutt_buffer_pool_release(&buf);
329 FREE(&simple);
330 mutt_pattern_free(&pat);
331
332 return rc;
333 }
334
335 /**
336 * mutt_pattern_func - Perform some Pattern matching
337 * @param ctx Current Mailbox
338 * @param op Operation to perform, e.g. #MUTT_LIMIT
339 * @param prompt Prompt to show the user
340 * @retval 0 Success
341 * @retval -1 Failure
342 */
mutt_pattern_func(struct Context * ctx,int op,char * prompt)343 int mutt_pattern_func(struct Context *ctx, int op, char *prompt)
344 {
345 if (!ctx || !ctx->mailbox)
346 return -1;
347
348 struct Mailbox *m = ctx->mailbox;
349
350 struct Buffer err;
351 int rc = -1;
352 struct Progress *progress = NULL;
353 struct Buffer *buf = mutt_buffer_pool_get();
354
355 mutt_buffer_strcpy(buf, NONULL(ctx->pattern));
356 if (prompt || (op != MUTT_LIMIT))
357 {
358 if ((mutt_buffer_get_field(prompt, buf, MUTT_PATTERN | MUTT_CLEAR, false,
359 NULL, NULL, NULL) != 0) ||
360 mutt_buffer_is_empty(buf))
361 {
362 mutt_buffer_pool_release(&buf);
363 return -1;
364 }
365 }
366
367 mutt_message(_("Compiling search pattern..."));
368
369 char *simple = mutt_buffer_strdup(buf);
370 const char *const c_simple_search =
371 cs_subset_string(NeoMutt->sub, "simple_search");
372 mutt_check_simple(buf, NONULL(c_simple_search));
373 const char *pbuf = buf->data;
374 while (*pbuf == ' ')
375 pbuf++;
376 const bool match_all = mutt_str_equal(pbuf, "~A");
377
378 mutt_buffer_init(&err);
379 err.dsize = 256;
380 err.data = mutt_mem_malloc(err.dsize);
381 struct PatternList *pat =
382 mutt_pattern_comp(m, ctx->menu, buf->data, MUTT_PC_FULL_MSG, &err);
383 if (!pat)
384 {
385 mutt_error("%s", err.data);
386 goto bail;
387 }
388
389 #ifdef USE_IMAP
390 if ((m->type == MUTT_IMAP) && (!imap_search(m, pat)))
391 goto bail;
392 #endif
393
394 progress = progress_new(_("Executing command on matching messages..."), MUTT_PROGRESS_READ,
395 (op == MUTT_LIMIT) ? m->msg_count : m->vcount);
396
397 if (op == MUTT_LIMIT)
398 {
399 m->vcount = 0;
400 ctx->vsize = 0;
401 ctx->collapsed = false;
402 int padding = mx_msg_padding_size(m);
403
404 for (int i = 0; i < m->msg_count; i++)
405 {
406 struct Email *e = m->emails[i];
407 if (!e)
408 break;
409
410 progress_update(progress, i, -1);
411 /* new limit pattern implicitly uncollapses all threads */
412 e->vnum = -1;
413 e->visible = false;
414 e->collapsed = false;
415 e->num_hidden = 0;
416 if (match_all ||
417 mutt_pattern_exec(SLIST_FIRST(pat), MUTT_MATCH_FULL_ADDRESS, m, e, NULL))
418 {
419 e->vnum = m->vcount;
420 e->visible = true;
421 m->v2r[m->vcount] = i;
422 m->vcount++;
423 struct Body *b = e->body;
424 ctx->vsize += b->length + b->offset - b->hdr_offset + padding;
425 }
426 }
427 }
428 else
429 {
430 for (int i = 0; i < m->vcount; i++)
431 {
432 struct Email *e = mutt_get_virt_email(m, i);
433 if (!e)
434 continue;
435 progress_update(progress, i, -1);
436 if (mutt_pattern_exec(SLIST_FIRST(pat), MUTT_MATCH_FULL_ADDRESS, m, e, NULL))
437 {
438 switch (op)
439 {
440 case MUTT_UNDELETE:
441 mutt_set_flag(m, e, MUTT_PURGE, false);
442 /* fallthrough */
443 case MUTT_DELETE:
444 mutt_set_flag(m, e, MUTT_DELETE, (op == MUTT_DELETE));
445 break;
446 case MUTT_TAG:
447 case MUTT_UNTAG:
448 mutt_set_flag(m, e, MUTT_TAG, (op == MUTT_TAG));
449 break;
450 }
451 }
452 }
453 }
454 progress_free(&progress);
455
456 mutt_clear_error();
457
458 if (op == MUTT_LIMIT)
459 {
460 /* drop previous limit pattern */
461 FREE(&ctx->pattern);
462 mutt_pattern_free(&ctx->limit_pattern);
463
464 if (m->msg_count && !m->vcount)
465 mutt_error(_("No messages matched criteria"));
466
467 /* record new limit pattern, unless match all */
468 if (!match_all)
469 {
470 ctx->pattern = simple;
471 simple = NULL; /* don't clobber it */
472 ctx->limit_pattern =
473 mutt_pattern_comp(m, ctx->menu, buf->data, MUTT_PC_FULL_MSG, &err);
474 }
475 }
476
477 rc = 0;
478
479 bail:
480 mutt_buffer_pool_release(&buf);
481 FREE(&simple);
482 mutt_pattern_free(&pat);
483 FREE(&err.data);
484
485 return rc;
486 }
487
488 /**
489 * mutt_search_command - Perform a search
490 * @param m Mailbox to search through
491 * @param menu Current Menu
492 * @param cur Index number of current email
493 * @param op Operation to perform, e.g. OP_SEARCH_NEXT
494 * @retval >=0 Index of matching email
495 * @retval -1 No match, or error
496 */
mutt_search_command(struct Mailbox * m,struct Menu * menu,int cur,int op)497 int mutt_search_command(struct Mailbox *m, struct Menu *menu, int cur, int op)
498 {
499 struct Progress *progress = NULL;
500 int rc = -1;
501
502 if ((*LastSearch == '\0') || ((op != OP_SEARCH_NEXT) && (op != OP_SEARCH_OPPOSITE)))
503 {
504 char buf[256];
505 mutt_str_copy(buf, (LastSearch[0] != '\0') ? LastSearch : "", sizeof(buf));
506 if ((mutt_get_field(
507 ((op == OP_SEARCH) || (op == OP_SEARCH_NEXT)) ? _("Search for: ") : _("Reverse search for: "),
508 buf, sizeof(buf), MUTT_CLEAR | MUTT_PATTERN, false, NULL, NULL) != 0) ||
509 (buf[0] == '\0'))
510 {
511 return -1;
512 }
513
514 if ((op == OP_SEARCH) || (op == OP_SEARCH_NEXT))
515 OptSearchReverse = false;
516 else
517 OptSearchReverse = true;
518
519 /* compare the *expanded* version of the search pattern in case
520 * $simple_search has changed while we were searching */
521 struct Buffer *tmp = mutt_buffer_pool_get();
522 mutt_buffer_strcpy(tmp, buf);
523 const char *const c_simple_search =
524 cs_subset_string(NeoMutt->sub, "simple_search");
525 mutt_check_simple(tmp, NONULL(c_simple_search));
526
527 if (!SearchPattern || !mutt_str_equal(mutt_buffer_string(tmp), LastSearchExpn))
528 {
529 struct Buffer err;
530 mutt_buffer_init(&err);
531 OptSearchInvalid = true;
532 mutt_str_copy(LastSearch, buf, sizeof(LastSearch));
533 mutt_str_copy(LastSearchExpn, mutt_buffer_string(tmp), sizeof(LastSearchExpn));
534 mutt_message(_("Compiling search pattern..."));
535 mutt_pattern_free(&SearchPattern);
536 err.dsize = 256;
537 err.data = mutt_mem_malloc(err.dsize);
538 SearchPattern = mutt_pattern_comp(m, menu, tmp->data, MUTT_PC_FULL_MSG, &err);
539 if (!SearchPattern)
540 {
541 mutt_buffer_pool_release(&tmp);
542 mutt_error("%s", err.data);
543 FREE(&err.data);
544 LastSearch[0] = '\0';
545 LastSearchExpn[0] = '\0';
546 return -1;
547 }
548 FREE(&err.data);
549 mutt_clear_error();
550 }
551
552 mutt_buffer_pool_release(&tmp);
553 }
554
555 if (OptSearchInvalid)
556 {
557 for (int i = 0; i < m->msg_count; i++)
558 m->emails[i]->searched = false;
559 #ifdef USE_IMAP
560 if ((m->type == MUTT_IMAP) && (!imap_search(m, SearchPattern)))
561 return -1;
562 #endif
563 OptSearchInvalid = false;
564 }
565
566 int incr = OptSearchReverse ? -1 : 1;
567 if (op == OP_SEARCH_OPPOSITE)
568 incr = -incr;
569
570 progress = progress_new(_("Searching..."), MUTT_PROGRESS_READ, m->vcount);
571
572 for (int i = cur + incr, j = 0; j != m->vcount; j++)
573 {
574 const char *msg = NULL;
575 progress_update(progress, j, -1);
576 const bool c_wrap_search = cs_subset_bool(NeoMutt->sub, "wrap_search");
577 if (i > m->vcount - 1)
578 {
579 i = 0;
580 if (c_wrap_search)
581 msg = _("Search wrapped to top");
582 else
583 {
584 mutt_message(_("Search hit bottom without finding match"));
585 goto done;
586 }
587 }
588 else if (i < 0)
589 {
590 i = m->vcount - 1;
591 if (c_wrap_search)
592 msg = _("Search wrapped to bottom");
593 else
594 {
595 mutt_message(_("Search hit top without finding match"));
596 goto done;
597 }
598 }
599
600 struct Email *e = mutt_get_virt_email(m, i);
601 if (e->searched)
602 {
603 /* if we've already evaluated this message, use the cached value */
604 if (e->matched)
605 {
606 mutt_clear_error();
607 if (msg && *msg)
608 mutt_message(msg);
609 rc = i;
610 goto done;
611 }
612 }
613 else
614 {
615 /* remember that we've already searched this message */
616 e->searched = true;
617 e->matched = mutt_pattern_exec(SLIST_FIRST(SearchPattern),
618 MUTT_MATCH_FULL_ADDRESS, m, e, NULL);
619 if (e->matched > 0)
620 {
621 mutt_clear_error();
622 if (msg && *msg)
623 mutt_message(msg);
624 rc = i;
625 goto done;
626 }
627 }
628
629 if (SigInt)
630 {
631 mutt_error(_("Search interrupted"));
632 SigInt = false;
633 goto done;
634 }
635
636 i += incr;
637 }
638
639 mutt_error(_("Not found"));
640 done:
641 progress_free(&progress);
642 return rc;
643 }
644
645 /**
646 * mutt_search_alias_command - Perform a search
647 * @param menu Menu to search through
648 * @param cur Index number of current alias
649 * @param op Operation to perform, e.g. OP_SEARCH_NEXT
650 * @retval >=0 Index of matching alias
651 * @retval -1 No match, or error
652 */
mutt_search_alias_command(struct Menu * menu,int cur,int op)653 int mutt_search_alias_command(struct Menu *menu, int cur, int op)
654 {
655 struct Progress *progress = NULL;
656 const struct AliasMenuData *mdata = menu->mdata;
657 const struct AliasViewArray *ava = &mdata->ava;
658 int rc = -1;
659
660 if ((*LastSearch == '\0') || ((op != OP_SEARCH_NEXT) && (op != OP_SEARCH_OPPOSITE)))
661 {
662 char buf[256];
663 mutt_str_copy(buf, (LastSearch[0] != '\0') ? LastSearch : "", sizeof(buf));
664 if ((mutt_get_field(
665 ((op == OP_SEARCH) || (op == OP_SEARCH_NEXT)) ? _("Search for: ") : _("Reverse search for: "),
666 buf, sizeof(buf), MUTT_CLEAR | MUTT_PATTERN, false, NULL, NULL) != 0) ||
667 (buf[0] == '\0'))
668 {
669 return -1;
670 }
671
672 if ((op == OP_SEARCH) || (op == OP_SEARCH_NEXT))
673 OptSearchReverse = false;
674 else
675 OptSearchReverse = true;
676
677 /* compare the *expanded* version of the search pattern in case
678 * $simple_search has changed while we were searching */
679 struct Buffer *tmp = mutt_buffer_pool_get();
680 mutt_buffer_strcpy(tmp, buf);
681 mutt_check_simple(tmp, MUTT_ALIAS_SIMPLESEARCH);
682
683 if (!SearchPattern || !mutt_str_equal(mutt_buffer_string(tmp), LastSearchExpn))
684 {
685 struct Buffer err;
686 mutt_buffer_init(&err);
687 OptSearchInvalid = true;
688 mutt_str_copy(LastSearch, buf, sizeof(LastSearch));
689 mutt_str_copy(LastSearchExpn, mutt_buffer_string(tmp), sizeof(LastSearchExpn));
690 mutt_message(_("Compiling search pattern..."));
691 mutt_pattern_free(&SearchPattern);
692 err.dsize = 256;
693 err.data = mutt_mem_malloc(err.dsize);
694 SearchPattern = mutt_pattern_comp(NULL, menu, tmp->data, MUTT_PC_FULL_MSG, &err);
695 if (!SearchPattern)
696 {
697 mutt_buffer_pool_release(&tmp);
698 mutt_error("%s", err.data);
699 FREE(&err.data);
700 LastSearch[0] = '\0';
701 LastSearchExpn[0] = '\0';
702 return -1;
703 }
704 FREE(&err.data);
705 mutt_clear_error();
706 }
707
708 mutt_buffer_pool_release(&tmp);
709 }
710
711 if (OptSearchInvalid)
712 {
713 struct AliasView *av = NULL;
714 ARRAY_FOREACH(av, ava)
715 {
716 av->is_searched = false;
717 }
718
719 OptSearchInvalid = false;
720 }
721
722 int incr = OptSearchReverse ? -1 : 1;
723 if (op == OP_SEARCH_OPPOSITE)
724 incr = -incr;
725
726 progress = progress_new(_("Searching..."), MUTT_PROGRESS_READ, ARRAY_SIZE(ava));
727
728 for (int i = cur + incr, j = 0; j != ARRAY_SIZE(ava); j++)
729 {
730 const char *msg = NULL;
731 progress_update(progress, j, -1);
732 const bool c_wrap_search = cs_subset_bool(NeoMutt->sub, "wrap_search");
733 if (i > ARRAY_SIZE(ava) - 1)
734 {
735 i = 0;
736 if (c_wrap_search)
737 msg = _("Search wrapped to top");
738 else
739 {
740 mutt_message(_("Search hit bottom without finding match"));
741 goto done;
742 }
743 }
744 else if (i < 0)
745 {
746 i = ARRAY_SIZE(ava) - 1;
747 if (c_wrap_search)
748 msg = _("Search wrapped to bottom");
749 else
750 {
751 mutt_message(_("Search hit top without finding match"));
752 goto done;
753 }
754 }
755
756 struct AliasView *av = ARRAY_GET(ava, i);
757 if (av->is_searched)
758 {
759 /* if we've already evaluated this message, use the cached value */
760 if (av->is_matched)
761 {
762 mutt_clear_error();
763 if (msg && *msg)
764 mutt_message(msg);
765 rc = i;
766 goto done;
767 }
768 }
769 else
770 {
771 /* remember that we've already searched this message */
772 av->is_searched = true;
773 av->is_matched = mutt_pattern_alias_exec(SLIST_FIRST(SearchPattern),
774 MUTT_MATCH_FULL_ADDRESS, av, NULL);
775 if (av->is_matched > 0)
776 {
777 mutt_clear_error();
778 if (msg && *msg)
779 mutt_message(msg);
780 rc = i;
781 goto done;
782 }
783 }
784
785 if (SigInt)
786 {
787 mutt_error(_("Search interrupted"));
788 SigInt = false;
789 goto done;
790 }
791
792 i += incr;
793 }
794
795 mutt_error(_("Not found"));
796 done:
797 progress_free(&progress);
798 return rc;
799 }
800