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