• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

neomutt / neomutt / 25650723386

09 May 2026 09:51AM UTC coverage: 42.586% (-0.02%) from 42.608%
25650723386

push

github

flatcap
pager: fix broken message-hook

12459 of 29256 relevant lines covered (42.59%)

5222.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.05
/pattern/compile.c
1
/**
2
 * @file
3
 * Compile a Pattern
4
 *
5
 * @authors
6
 * Copyright (C) 2020 R Primus <rprimus@gmail.com>
7
 * Copyright (C) 2020-2022 Pietro Cerutti <gahr@gahr.ch>
8
 * Copyright (C) 2020-2025 Richard Russon <rich@flatcap.org>
9
 * Copyright (C) 2025 Thomas Klausner <wiz@gatalith.at>
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 pattern_compile Compile a Pattern
28
 *
29
 * Compile a Pattern
30
 */
31

32
#include "config.h"
33
#include <stdbool.h>
34
#include <stdint.h>
35
#include <stdio.h>
36
#include <stdlib.h>
37
#include <string.h>
38
#include <sys/types.h>
39
#include <time.h>
40
#include "private.h"
41
#include "mutt/lib.h"
42
#include "address/lib.h"
43
#include "config/lib.h"
44
#include "core/lib.h"
45
#include "alias/lib.h"
46
#include "gui/lib.h"
47
#include "lib.h"
48
#include "parse/lib.h"
49

50
/**
51
 * enum ParseDateRangeFlag - Flags for parse_date_range(), e.g. #MUTT_PDR_MINUS
52
 */
53
enum ParseDateRangeFlag
54
{
55
  // clang-format off
56
  MUTT_PDR_NONE     =       0,  ///< No flags are set
57
  MUTT_PDR_MINUS    = 1U << 0,  ///< Pattern contains a range
58
  MUTT_PDR_PLUS     = 1U << 1,  ///< Extend the range using '+'
59
  MUTT_PDR_WINDOW   = 1U << 2,  ///< Extend the range in both directions using '*'
60
  MUTT_PDR_ABSOLUTE = 1U << 3,  ///< Absolute pattern range
61
  MUTT_PDR_DONE     = 1U << 4,  ///< Pattern parse successfully
62
  MUTT_PDR_ERROR    = 1U << 5,  ///< Invalid pattern
63
  // clang-format on
64
};
65
typedef uint8_t ParseDateRangeFlags;
66

67
#define MUTT_PDR_ERRORDONE (MUTT_PDR_ERROR | MUTT_PDR_DONE)
68

69
/**
70
 * eat_regex - Parse a regex - Implements ::eat_arg_t - @ingroup eat_arg_api
71
 */
72
static bool eat_regex(struct Pattern *pat, PatternCompFlags flags,
23✔
73
                      struct Buffer *s, struct Buffer *err)
74
{
75
  struct Buffer *token = buf_pool_get();
23✔
76
  bool rc = false;
77
  const char *pexpr = s->dptr;
23✔
78
  if ((parse_extract_token(token, s, TOKEN_PATTERN | TOKEN_COMMENT) != 0) ||
23✔
79
      !token->data)
23✔
80
  {
81
    buf_printf(err, _("Error in expression: %s"), pexpr);
×
82
    goto out;
×
83
  }
84
  if (buf_is_empty(token))
23✔
85
  {
86
    buf_addstr(err, _("Empty expression"));
×
87
    goto out;
×
88
  }
89

90
  pat->p.regex = MUTT_MEM_CALLOC(1, regex_t);
23✔
91
#ifdef USE_DEBUG_GRAPHVIZ
92
  pat->raw_pattern = buf_strdup(token);
93
#endif
94
  uint16_t case_flags = mutt_mb_is_lower(token->data) ? REG_ICASE : 0;
23✔
95
  int rc2 = REG_COMP(pat->p.regex, token->data, REG_NEWLINE | REG_NOSUB | case_flags);
23✔
96
  if (rc2 != 0)
23✔
97
  {
98
    char errmsg[256] = { 0 };
×
99
    regerror(rc2, pat->p.regex, errmsg, sizeof(errmsg));
×
100
    buf_printf(err, "'%s': %s", token->data, errmsg);
×
101
    FREE(&pat->p.regex);
×
102
    goto out;
×
103
  }
104

105
  rc = true;
106

107
out:
23✔
108
  buf_pool_release(&token);
23✔
109
  return rc;
23✔
110
}
111

112
/**
113
 * eat_string - Parse a plain string - Implements ::eat_arg_t - @ingroup eat_arg_api
114
 */
115
static bool eat_string(struct Pattern *pat, PatternCompFlags flags,
14✔
116
                       struct Buffer *s, struct Buffer *err)
117
{
118
  struct Buffer *token = buf_pool_get();
14✔
119
  bool rc = false;
120
  const char *pexpr = s->dptr;
14✔
121
  if ((parse_extract_token(token, s, TOKEN_PATTERN | TOKEN_COMMENT) != 0) ||
14✔
122
      !token->data)
14✔
123
  {
124
    buf_printf(err, _("Error in expression: %s"), pexpr);
×
125
    goto out;
×
126
  }
127
  if (buf_is_empty(token))
14✔
128
  {
129
    buf_addstr(err, _("Empty expression"));
×
130
    goto out;
×
131
  }
132

133
  pat->string_match = true;
14✔
134
  pat->p.str = buf_strdup(token);
14✔
135
  pat->ign_case = mutt_mb_is_lower(token->data);
14✔
136

137
  rc = true;
138

139
out:
14✔
140
  buf_pool_release(&token);
14✔
141
  return rc;
14✔
142
}
143

144
/**
145
 * eat_group - Parse a group name - Implements ::eat_arg_t - @ingroup eat_arg_api
146
 */
147
static bool eat_group(struct Pattern *pat, PatternCompFlags flags,
×
148
                      struct Buffer *s, struct Buffer *err)
149
{
150
  struct Buffer *token = buf_pool_get();
×
151
  bool rc = false;
152
  const char *pexpr = s->dptr;
×
153
  if ((parse_extract_token(token, s, TOKEN_PATTERN | TOKEN_COMMENT) != 0) ||
×
154
      !token->data)
×
155
  {
156
    buf_printf(err, _("Error in expression: %s"), pexpr);
×
157
    goto out;
×
158
  }
159
  if (buf_is_empty(token))
×
160
  {
161
    buf_addstr(err, _("Empty expression"));
×
162
    goto out;
×
163
  }
164

165
  struct AliasModuleData *mod_data = neomutt_get_module_data(NeoMutt, MODULE_ID_ALIAS);
×
166
  ASSERT(mod_data);
×
167

168
  pat->group_match = true;
×
169
  pat->p.group = groups_get_group(mod_data->groups, token->data);
×
170

171
  rc = true;
172

173
out:
×
174
  buf_pool_release(&token);
×
175
  return rc;
×
176
}
177

178
/**
179
 * add_query_msgid - Parse a Message-ID and add it to a list - Implements ::mutt_file_map_t - @ingroup mutt_file_map_api
180
 * @retval true Always
181
 */
182
static bool add_query_msgid(char *line, int line_num, void *user_data)
×
183
{
184
  struct ListHead *msgid_list = (struct ListHead *) (user_data);
185
  char *nows = mutt_str_skip_whitespace(line);
×
186
  if (*nows == '\0')
×
187
    return true;
188
  mutt_str_remove_trailing_ws(nows);
×
189
  mutt_list_insert_tail(msgid_list, mutt_str_dup(nows));
×
190
  return true;
×
191
}
192

193
/**
194
 * eat_query - Parse a query for an external search program - Implements ::eat_arg_t - @ingroup eat_arg_api
195
 * @param pat   Pattern to store the results in
196
 * @param flags Flags, e.g. #MUTT_PC_PATTERN_DYNAMIC
197
 * @param s     String to parse
198
 * @param err   Buffer for error messages
199
 * @param m     Mailbox
200
 * @retval true The pattern was read successfully
201
 */
202
static bool eat_query(struct Pattern *pat, PatternCompFlags flags,
1✔
203
                      struct Buffer *s, struct Buffer *err, struct Mailbox *m)
204
{
205
  struct Buffer *cmd = buf_pool_get();
1✔
206
  struct Buffer *tok = buf_pool_get();
1✔
207
  bool rc = false;
208

209
  FILE *fp = NULL;
1✔
210

211
  const char *const c_external_search_command = cs_subset_string(NeoMutt->sub, "external_search_command");
1✔
212
  if (!c_external_search_command)
1✔
213
  {
214
    buf_addstr(err, _("No search command defined"));
1✔
215
    goto out;
1✔
216
  }
217

218
  char *pexpr = s->dptr;
×
219
  if ((parse_extract_token(tok, s, TOKEN_PATTERN | TOKEN_COMMENT) != 0) || !tok->data)
×
220
  {
221
    buf_printf(err, _("Error in expression: %s"), pexpr);
×
222
    goto out;
×
223
  }
224
  if (*tok->data == '\0')
×
225
  {
226
    buf_addstr(err, _("Empty expression"));
×
227
    goto out;
×
228
  }
229

230
  buf_addstr(cmd, c_external_search_command);
×
231
  buf_addch(cmd, ' ');
×
232

233
  if (m)
×
234
  {
235
    char *escaped_folder = mutt_path_escape(mailbox_path(m));
×
236
    mutt_debug(LL_DEBUG2, "escaped folder path: %s\n", escaped_folder);
×
237
    buf_addch(cmd, '\'');
×
238
    buf_addstr(cmd, escaped_folder);
×
239
    buf_addch(cmd, '\'');
×
240
  }
241
  else
242
  {
243
    buf_addch(cmd, '/');
×
244
  }
245
  buf_addch(cmd, ' ');
×
246
  buf_addstr(cmd, tok->data);
×
247

248
  mutt_message(_("Running search command: %s ..."), cmd->data);
×
249
  pat->is_multi = true;
×
250
  mutt_list_clear(&pat->p.multi_cases);
×
251
  pid_t pid = filter_create(cmd->data, NULL, &fp, NULL, NeoMutt->env);
×
252
  if (pid < 0)
×
253
  {
254
    buf_printf(err, "unable to fork command: %s\n", cmd->data);
×
255
    goto out;
×
256
  }
257

258
  mutt_file_map_lines(add_query_msgid, &pat->p.multi_cases, fp, MUTT_RL_NONE);
×
259
  mutt_file_fclose(&fp);
×
260
  filter_wait(pid);
×
261

262
  rc = true;
263

264
out:
1✔
265
  buf_pool_release(&cmd);
1✔
266
  buf_pool_release(&tok);
1✔
267
  return rc;
1✔
268
}
269

270
/**
271
 * get_offset - Calculate a symbolic offset
272
 * @param tm   Store the time here
273
 * @param s    string to parse
274
 * @param sign Sign of range, 1 for positive, -1 for negative
275
 * @retval ptr Next char after parsed offset
276
 *
277
 * - Ny years
278
 * - Nm months
279
 * - Nw weeks
280
 * - Nd days
281
 */
282
static const char *get_offset(struct tm *tm, const char *s, int sign)
16✔
283
{
284
  char *ps = NULL;
16✔
285
  int offset = strtol(s, &ps, 0);
16✔
286
  if (((sign < 0) && (offset > 0)) || ((sign > 0) && (offset < 0)))
16✔
287
    offset = -offset;
14✔
288

289
  switch (*ps)
16✔
290
  {
291
    case 'y':
2✔
292
      tm->tm_year += offset;
2✔
293
      break;
2✔
294
    case 'm':
2✔
295
      tm->tm_mon += offset;
2✔
296
      break;
2✔
297
    case 'w':
1✔
298
      tm->tm_mday += 7 * offset;
1✔
299
      break;
1✔
300
    case 'd':
6✔
301
      tm->tm_mday += offset;
6✔
302
      break;
6✔
303
    case 'H':
1✔
304
      tm->tm_hour += offset;
1✔
305
      break;
1✔
306
    case 'M':
1✔
307
      tm->tm_min += offset;
1✔
308
      break;
1✔
309
    case 'S':
1✔
310
      tm->tm_sec += offset;
1✔
311
      break;
1✔
312
    default:
313
      return s;
314
  }
315
  mutt_date_normalize_time(tm);
14✔
316
  return ps + 1;
14✔
317
}
318

319
/**
320
 * get_date - Parse a (partial) date in dd/mm/yyyy format
321
 * @param s   String to parse
322
 * @param t   Store the time here
323
 * @param err Buffer for error messages
324
 * @retval ptr First character after the date
325
 *
326
 * This function parses a (partial) date separated by '/'.  The month and year
327
 * are optional and if the year is less than 70 it's assumed to be after 2000.
328
 *
329
 * Examples:
330
 * - "10"         = 10 of this month, this year
331
 * - "10/12"      = 10 of December,   this year
332
 * - "10/12/04"   = 10 of December,   2004
333
 * - "10/12/2008" = 10 of December,   2008
334
 * - "20081210"   = 10 of December,   2008
335
 */
336
static const char *get_date(const char *s, struct tm *t, struct Buffer *err)
11✔
337
{
338
  char *p = NULL;
11✔
339
  struct tm tm = mutt_date_localtime(mutt_date_now());
11✔
340
  bool iso8601 = true;
341

342
  for (int v = 0; v < 8; v++)
51✔
343
  {
344
    if (s[v] && (s[v] >= '0') && (s[v] <= '9'))
48✔
345
      continue;
346

347
    iso8601 = false;
348
    break;
349
  }
350

351
  if (iso8601)
11✔
352
  {
353
    int year = 0;
3✔
354
    int month = 0;
3✔
355
    int mday = 0;
3✔
356
    sscanf(s, "%4d%2d%2d", &year, &month, &mday);
3✔
357

358
    t->tm_year = year;
3✔
359
    if (t->tm_year > 1900)
3✔
360
      t->tm_year -= 1900;
3✔
361
    t->tm_mon = month - 1;
3✔
362
    t->tm_mday = mday;
3✔
363

364
    if ((t->tm_mday < 1) || (t->tm_mday > 31))
3✔
365
    {
366
      buf_printf(err, _("Invalid day of month: %s"), s);
1✔
367
      return NULL;
1✔
368
    }
369
    if ((t->tm_mon < 0) || (t->tm_mon > 11))
2✔
370
    {
371
      buf_printf(err, _("Invalid month: %s"), s);
1✔
372
      return NULL;
1✔
373
    }
374

375
    return (s + 8);
1✔
376
  }
377

378
  t->tm_mday = strtol(s, &p, 10);
8✔
379
  if ((t->tm_mday < 1) || (t->tm_mday > 31))
8✔
380
  {
381
    buf_printf(err, _("Invalid day of month: %s"), s);
1✔
382
    return NULL;
1✔
383
  }
384
  if (*p != '/')
7✔
385
  {
386
    /* fill in today's month and year */
387
    t->tm_mon = tm.tm_mon;
×
388
    t->tm_year = tm.tm_year;
×
389
    return p;
×
390
  }
391
  p++;
7✔
392
  t->tm_mon = strtol(p, &p, 10) - 1;
7✔
393
  if ((t->tm_mon < 0) || (t->tm_mon > 11))
7✔
394
  {
395
    buf_printf(err, _("Invalid month: %s"), p);
1✔
396
    return NULL;
1✔
397
  }
398
  if (*p != '/')
6✔
399
  {
400
    t->tm_year = tm.tm_year;
×
401
    return p;
×
402
  }
403
  p++;
6✔
404
  t->tm_year = strtol(p, &p, 10);
6✔
405
  if (t->tm_year < 70) /* year 2000+ */
6✔
406
    t->tm_year += 100;
×
407
  else if (t->tm_year > 1900)
6✔
408
    t->tm_year -= 1900;
6✔
409
  return p;
6✔
410
}
411

412
/**
413
 * parse_date_range - Parse a date range
414
 * @param pc       String to parse
415
 * @param min      Earlier date
416
 * @param max      Later date
417
 * @param have_min Do we have a base minimum date?
418
 * @param base_min Base minimum date
419
 * @param err      Buffer for error messages
420
 * @retval ptr First character after the date
421
 */
422
static const char *parse_date_range(const char *pc, struct tm *min, struct tm *max,
5✔
423
                                    bool have_min, struct tm *base_min, struct Buffer *err)
424
{
425
  ParseDateRangeFlags flags = MUTT_PDR_NONE;
426
  while (*pc && ((flags & MUTT_PDR_DONE) == 0))
9✔
427
  {
428
    const char *pt = NULL;
429
    char ch = *pc++;
4✔
430
    SKIPWS(pc);
4✔
431
    switch (ch)
4✔
432
    {
433
      case '-':
2✔
434
      {
435
        /* try a range of absolute date minus offset of Ndwmy */
436
        pt = get_offset(min, pc, -1);
2✔
437
        if (pc == pt)
2✔
438
        {
439
          if (flags == MUTT_PDR_NONE)
2✔
440
          { /* nothing yet and no offset parsed => absolute date? */
441
            if (!get_date(pc, max, err))
2✔
442
            {
443
              flags |= (MUTT_PDR_ABSOLUTE | MUTT_PDR_ERRORDONE); /* done bad */
444
            }
445
            else
446
            {
447
              /* reestablish initial base minimum if not specified */
448
              if (!have_min)
2✔
449
                memcpy(min, base_min, sizeof(struct tm));
450
              flags |= (MUTT_PDR_ABSOLUTE | MUTT_PDR_DONE); /* done good */
451
            }
452
          }
453
          else
454
          {
455
            flags |= MUTT_PDR_ERRORDONE;
×
456
          }
457
        }
458
        else
459
        {
460
          pc = pt;
461
          if ((flags == MUTT_PDR_NONE) && !have_min)
×
462
          { /* the very first "-3d" without a previous absolute date */
463
            max->tm_year = min->tm_year;
×
464
            max->tm_mon = min->tm_mon;
×
465
            max->tm_mday = min->tm_mday;
×
466
          }
467
          flags |= MUTT_PDR_MINUS;
×
468
        }
469
        break;
470
      }
471
      case '+':
1✔
472
      { /* enlarge plus range */
473
        pt = get_offset(max, pc, 1);
1✔
474
        if (pc == pt)
1✔
475
        {
476
          flags |= MUTT_PDR_ERRORDONE;
×
477
        }
478
        else
479
        {
480
          pc = pt;
481
          flags |= MUTT_PDR_PLUS;
1✔
482
        }
483
        break;
484
      }
485
      case '*':
1✔
486
      { /* enlarge window in both directions */
487
        pt = get_offset(min, pc, -1);
1✔
488
        if (pc == pt)
1✔
489
        {
490
          flags |= MUTT_PDR_ERRORDONE;
×
491
        }
492
        else
493
        {
494
          pc = get_offset(max, pc, 1);
1✔
495
          flags |= MUTT_PDR_WINDOW;
1✔
496
        }
497
        break;
498
      }
499
      default:
×
500
        flags |= MUTT_PDR_ERRORDONE;
×
501
    }
502
    SKIPWS(pc);
4✔
503
  }
504
  if ((flags & MUTT_PDR_ERROR) && !(flags & MUTT_PDR_ABSOLUTE))
5✔
505
  { /* get_date has its own error message, don't overwrite it here */
506
    buf_printf(err, _("Invalid relative date: %s"), pc - 1);
×
507
  }
508
  return (flags & MUTT_PDR_ERROR) ? NULL : pc;
5✔
509
}
510

511
/**
512
 * adjust_date_range - Put a date range in the correct order
513
 * @param[in,out] min Earlier date
514
 * @param[in,out] max Later date
515
 */
516
static void adjust_date_range(struct tm *min, struct tm *max)
16✔
517
{
518
  if ((min->tm_year > max->tm_year) ||
16✔
519
      ((min->tm_year == max->tm_year) && (min->tm_mon > max->tm_mon)) ||
2✔
520
      ((min->tm_year == max->tm_year) && (min->tm_mon == max->tm_mon) &&
15✔
521
       (min->tm_mday > max->tm_mday)))
2✔
522
  {
523
    int tmp;
524

525
    tmp = min->tm_year;
526
    min->tm_year = max->tm_year;
1✔
527
    max->tm_year = tmp;
1✔
528

529
    tmp = min->tm_mon;
1✔
530
    min->tm_mon = max->tm_mon;
1✔
531
    max->tm_mon = tmp;
1✔
532

533
    tmp = min->tm_mday;
1✔
534
    min->tm_mday = max->tm_mday;
1✔
535
    max->tm_mday = tmp;
1✔
536

537
    min->tm_hour = 0;
1✔
538
    min->tm_min = 0;
1✔
539
    min->tm_sec = 0;
1✔
540
    max->tm_hour = 23;
1✔
541
    max->tm_min = 59;
1✔
542
    max->tm_sec = 59;
1✔
543
  }
544
}
16✔
545

546
/**
547
 * eval_date_minmax - Evaluate a date-range pattern against 'now'
548
 * @param pat Pattern to modify
549
 * @param s   Pattern string to use
550
 * @param err Buffer for error messages
551
 * @retval true  Pattern valid and updated
552
 * @retval false Pattern invalid
553
 */
554
bool eval_date_minmax(struct Pattern *pat, const char *s, struct Buffer *err)
20✔
555
{
556
  /* the '0' time is Jan 1, 1970 UTC, so in order to prevent a negative time
557
   * when doing timezone conversion, we use Jan 2, 1970 UTC as the base here */
558
  struct tm min = { 0 };
20✔
559
  min.tm_mday = 2;
20✔
560
  min.tm_year = 70;
20✔
561

562
  /* Arbitrary year in the future.  Don't set this too high or
563
   * mutt_date_make_time() returns something larger than will fit in a time_t
564
   * on some systems */
565
  struct tm max = { 0 };
20✔
566
  max.tm_year = 137;
20✔
567
  max.tm_mon = 11;
20✔
568
  max.tm_mday = 31;
20✔
569
  max.tm_hour = 23;
20✔
570
  max.tm_min = 59;
20✔
571
  max.tm_sec = 59;
20✔
572

573
  if (strchr("<>=", s[0]))
20✔
574
  {
575
    /* offset from current time
576
     *  <3d  less than three days ago
577
     *  >3d  more than three days ago
578
     *  =3d  exactly three days ago */
579
    struct tm *tm = NULL;
580
    bool exact = false;
581

582
    if (s[0] == '<')
11✔
583
    {
584
      min = mutt_date_localtime(mutt_date_now());
11✔
585
      tm = &min;
586
    }
587
    else
588
    {
589
      max = mutt_date_localtime(mutt_date_now());
×
590
      tm = &max;
591

592
      if (s[0] == '=')
×
593
        exact = true;
594
    }
595

596
    /* Reset the HMS unless we are relative matching using one of those
597
     * offsets. */
598
    char *offset_type = NULL;
11✔
599
    strtol(s + 1, &offset_type, 0);
11✔
600
    if (!(*offset_type && strchr("HMS", *offset_type)))
11✔
601
    {
602
      tm->tm_hour = 23;
8✔
603
      tm->tm_min = 59;
8✔
604
      tm->tm_sec = 59;
8✔
605
    }
606

607
    /* force negative offset */
608
    get_offset(tm, s + 1, -1);
11✔
609

610
    if (exact)
11✔
611
    {
612
      /* start at the beginning of the day in question */
613
      memcpy(&min, &max, sizeof(max));
614
      min.tm_hour = 0;
×
615
      min.tm_sec = 0;
×
616
      min.tm_min = 0;
×
617
    }
618
  }
619
  else
620
  {
621
    const char *pc = s;
622

623
    bool have_min = false;
624
    bool until_now = false;
625
    if (mutt_isdigit(*pc))
9✔
626
    {
627
      /* minimum date specified */
628
      pc = get_date(pc, &min, err);
9✔
629
      if (!pc)
9✔
630
      {
631
        return false;
632
      }
633
      have_min = true;
634
      SKIPWS(pc);
5✔
635
      if (*pc == '-')
5✔
636
      {
637
        const char *pt = pc + 1;
2✔
638
        SKIPWS(pt);
2✔
639
        until_now = (*pt == '\0');
2✔
640
      }
641
    }
642

643
    if (!until_now)
5✔
644
    { /* max date or relative range/window */
645

646
      struct tm base_min = { 0 };
5✔
647

648
      if (!have_min)
5✔
649
      { /* save base minimum and set current date, e.g. for "-3d+1d" */
650
        memcpy(&base_min, &min, sizeof(base_min));
651
        min = mutt_date_localtime(mutt_date_now());
×
652
        min.tm_hour = 0;
×
653
        min.tm_sec = 0;
×
654
        min.tm_min = 0;
×
655
      }
656

657
      /* preset max date for relative offsets,
658
       * if nothing follows we search for messages on a specific day */
659
      max.tm_year = min.tm_year;
5✔
660
      max.tm_mon = min.tm_mon;
5✔
661
      max.tm_mday = min.tm_mday;
5✔
662

663
      if (!parse_date_range(pc, &min, &max, have_min, &base_min, err))
5✔
664
      { /* bail out on any parsing error */
665
        return false;
×
666
      }
667
    }
668
  }
669

670
  /* Since we allow two dates to be specified we'll have to adjust that. */
671
  adjust_date_range(&min, &max);
16✔
672

673
  pat->min = mutt_date_make_time(&min, true);
16✔
674
  pat->max = mutt_date_make_time(&max, true);
16✔
675

676
  return true;
16✔
677
}
678

679
/**
680
 * eat_range - Parse a number range - Implements ::eat_arg_t - @ingroup eat_arg_api
681
 */
682
static bool eat_range(struct Pattern *pat, PatternCompFlags flags,
3✔
683
                      struct Buffer *s, struct Buffer *err)
684
{
685
  char *tmp = NULL;
3✔
686
  bool do_exclusive = false;
687
  bool skip_quote = false;
688

689
  /* If simple_search is set to "~m %s", the range will have double quotes
690
   * around it...  */
691
  if (*s->dptr == '"')
3✔
692
  {
693
    s->dptr++;
×
694
    skip_quote = true;
695
  }
696
  if (*s->dptr == '<')
3✔
697
    do_exclusive = true;
698
  if ((*s->dptr != '-') && (*s->dptr != '<'))
3✔
699
  {
700
    /* range minimum */
701
    if (*s->dptr == '>')
2✔
702
    {
703
      pat->max = MUTT_MAXRANGE;
1✔
704
      pat->min = strtol(s->dptr + 1, &tmp, 0) + 1; /* exclusive range */
1✔
705
    }
706
    else
707
    {
708
      pat->min = strtol(s->dptr, &tmp, 0);
1✔
709
    }
710
    if (mutt_toupper(*tmp) == 'K') /* is there a prefix? */
2✔
711
    {
712
      pat->min *= 1024;
×
713
      tmp++;
×
714
    }
715
    else if (mutt_toupper(*tmp) == 'M')
2✔
716
    {
717
      pat->min *= 1048576;
×
718
      tmp++;
×
719
    }
720
    if (*s->dptr == '>')
2✔
721
    {
722
      s->dptr = tmp;
1✔
723
      return true;
1✔
724
    }
725
    if (*tmp != '-')
1✔
726
    {
727
      /* exact value */
728
      pat->max = pat->min;
×
729
      s->dptr = tmp;
×
730
      return true;
×
731
    }
732
    tmp++;
1✔
733
  }
734
  else
735
  {
736
    s->dptr++;
1✔
737
    tmp = s->dptr;
1✔
738
  }
739

740
  if (mutt_isdigit(*tmp))
2✔
741
  {
742
    /* range maximum */
743
    pat->max = strtol(tmp, &tmp, 0);
2✔
744
    if (mutt_toupper(*tmp) == 'K')
2✔
745
    {
746
      pat->max *= 1024;
1✔
747
      tmp++;
1✔
748
    }
749
    else if (mutt_toupper(*tmp) == 'M')
1✔
750
    {
751
      pat->max *= 1048576;
×
752
      tmp++;
×
753
    }
754
    if (do_exclusive)
2✔
755
      (pat->max)--;
1✔
756
  }
757
  else
758
  {
759
    pat->max = MUTT_MAXRANGE;
×
760
  }
761

762
  if (skip_quote && (*tmp == '"'))
2✔
763
    tmp++;
×
764

765
  SKIPWS(tmp);
2✔
766
  s->dptr = tmp;
2✔
767
  return true;
2✔
768
}
769

770
/**
771
 * eat_date - Parse a date pattern - Implements ::eat_arg_t - @ingroup eat_arg_api
772
 */
773
static bool eat_date(struct Pattern *pat, PatternCompFlags flags,
20✔
774
                     struct Buffer *s, struct Buffer *err)
775
{
776
  struct Buffer *tmp = buf_pool_get();
20✔
777
  bool rc = false;
778

779
  char *pexpr = s->dptr;
20✔
780
  if (parse_extract_token(tmp, s, TOKEN_COMMENT | TOKEN_PATTERN) != 0)
20✔
781
  {
782
    buf_printf(err, _("Error in expression: %s"), pexpr);
×
783
    goto out;
×
784
  }
785

786
  if (buf_is_empty(tmp))
20✔
787
  {
788
    buf_addstr(err, _("Empty expression"));
×
789
    goto out;
×
790
  }
791

792
  if (flags & MUTT_PC_PATTERN_DYNAMIC)
20✔
793
  {
794
    pat->dynamic = true;
3✔
795
    pat->p.str = buf_strdup(tmp);
3✔
796
  }
797

798
  rc = eval_date_minmax(pat, tmp->data, err);
20✔
799

800
out:
20✔
801
  buf_pool_release(&tmp);
20✔
802
  return rc;
20✔
803
}
804

805
/**
806
 * find_matching_paren - Find the matching parenthesis
807
 * @param s string to search
808
 * @retval ptr
809
 * - Matching close parenthesis
810
 * - End of string NUL, if not found
811
 */
812
static /* const */ char *find_matching_paren(/* const */ char *s)
8✔
813
{
814
  int level = 1;
815

816
  for (; *s; s++)
78✔
817
  {
818
    if (*s == '(')
78✔
819
    {
820
      level++;
×
821
    }
822
    else if (*s == ')')
78✔
823
    {
824
      level--;
8✔
825
      if (level == 0)
8✔
826
        break;
827
    }
828
  }
829
  return s;
8✔
830
}
831

832
/**
833
 * mutt_pattern_free - Free a Pattern
834
 * @param[out] pat Pattern to free
835
 */
836
void mutt_pattern_free(struct PatternList **pat)
247✔
837
{
838
  if (!pat || !*pat)
247✔
839
    return;
140✔
840

841
  struct Pattern *np = SLIST_FIRST(*pat);
107✔
842
  struct Pattern *next = NULL;
843

844
  while (np)
230✔
845
  {
846
    next = SLIST_NEXT(np, entries);
123✔
847

848
    if (np->is_multi)
123✔
849
    {
850
      mutt_list_free(&np->p.multi_cases);
×
851
    }
852
    else if (np->string_match || np->dynamic)
123✔
853
    {
854
      FREE(&np->p.str);
17✔
855
    }
856
    else if (np->group_match)
106✔
857
    {
858
      np->p.group = NULL;
×
859
    }
860
    else if (np->p.regex)
106✔
861
    {
862
      regfree(np->p.regex);
23✔
863
      FREE(&np->p.regex);
23✔
864
    }
865

866
#ifdef USE_DEBUG_GRAPHVIZ
867
    FREE(&np->raw_pattern);
868
#endif
869
    mutt_pattern_free(&np->child);
123✔
870
    FREE(&np);
123✔
871

872
    np = next;
123✔
873
  }
874

875
  FREE(pat);
107✔
876
}
877

878
/**
879
 * mutt_pattern_new - Create a new Pattern
880
 * @retval ptr Newly created Pattern
881
 */
882
static struct Pattern *mutt_pattern_new(void)
883
{
884
  return MUTT_MEM_CALLOC(1, struct Pattern);
14✔
885
}
886

887
/**
888
 * mutt_pattern_list_new - Create a new list containing a Pattern
889
 * @retval ptr Newly created list containing a single node with a Pattern
890
 */
891
static struct PatternList *mutt_pattern_list_new(void)
109✔
892
{
893
  struct PatternList *h = MUTT_MEM_CALLOC(1, struct PatternList);
109✔
894
  SLIST_INIT(h);
109✔
895
  struct Pattern *p = mutt_pattern_new();
896
  SLIST_INSERT_HEAD(h, p, entries);
109✔
897
  return h;
109✔
898
}
899

900
/**
901
 * attach_leaf - Attach a Pattern to a Pattern List
902
 * @param list Pattern List to attach to
903
 * @param leaf Pattern to attach
904
 * @retval ptr Attached leaf
905
 */
906
static struct Pattern *attach_leaf(struct PatternList *list, struct Pattern *leaf)
907
{
908
  struct Pattern *last = NULL;
909
  SLIST_FOREACH(last, list, entries)
17✔
910
  {
911
    // TODO - or we could use a doubly-linked list
912
    if (!SLIST_NEXT(last, entries))
17✔
913
    {
914
      SLIST_NEXT(last, entries) = leaf;
16✔
915
      break;
16✔
916
    }
917
  }
918
  return leaf;
919
}
920

921
/**
922
 * attach_new_root - Create a new Pattern as a parent for a List
923
 * @param curlist Pattern List
924
 * @retval ptr First Pattern in the original List
925
 *
926
 * @note curlist will be altered to the new root Pattern
927
 */
928
static struct Pattern *attach_new_root(struct PatternList **curlist)
929
{
930
  struct PatternList *root = mutt_pattern_list_new();
109✔
931
  struct Pattern *leaf = SLIST_FIRST(root);
109✔
932
  leaf->child = *curlist;
109✔
933
  *curlist = root;
109✔
934
  return leaf;
935
}
936

937
/**
938
 * attach_new_leaf - Attach a new Pattern to a List
939
 * @param curlist Pattern List
940
 * @retval ptr New Pattern in the original List
941
 *
942
 * @note curlist may be altered
943
 */
944
static struct Pattern *attach_new_leaf(struct PatternList **curlist)
108✔
945
{
946
  if (*curlist)
108✔
947
  {
948
    return attach_leaf(*curlist, mutt_pattern_new());
28✔
949
  }
950
  else
951
  {
952
    return attach_new_root(curlist);
94✔
953
  }
954
}
955

956
/**
957
 * mutt_pattern_comp - Create a Pattern
958
 * @param mv    Mailbox view
959
 * @param s     Pattern string
960
 * @param flags Flags, e.g. #MUTT_PC_FULL_MSG
961
 * @param err   Buffer for error messages
962
 * @retval ptr Newly allocated Pattern
963
 */
964
struct PatternList *mutt_pattern_comp(struct MailboxView *mv, const char *s,
104✔
965
                                      PatternCompFlags flags, struct Buffer *err)
966
{
967
  /* curlist when assigned will always point to a list containing at least one node
968
   * with a Pattern value.  */
969
  struct PatternList *curlist = NULL;
104✔
970
  bool pat_not = false;
971
  bool all_addr = false;
972
  bool pat_or = false;
973
  bool implicit = true; /* used to detect logical AND operator */
974
  bool is_alias = false;
975
  const struct PatternFlags *entry = NULL;
976
  char *p = NULL;
977
  char *buf = NULL;
104✔
978
  struct Mailbox *m = mv ? mv->mailbox : NULL;
104✔
979

980
  if (!s || (s[0] == '\0'))
104✔
981
  {
982
    buf_strcpy(err, _("empty pattern"));
1✔
983
    return NULL;
1✔
984
  }
985

986
  struct Buffer *ps = buf_pool_get();
103✔
987
  buf_strcpy(ps, s);
103✔
988
  buf_seek(ps, 0);
103✔
989

990
  SKIPWS(ps->dptr);
103✔
991
  while (*ps->dptr)
217✔
992
  {
993
    switch (*ps->dptr)
128✔
994
    {
995
      case '^':
×
996
        ps->dptr++;
×
997
        all_addr = !all_addr;
×
998
        break;
×
999
      case '!':
4✔
1000
        ps->dptr++;
4✔
1001
        pat_not = !pat_not;
4✔
1002
        break;
4✔
1003
      case '@':
×
1004
        ps->dptr++;
×
1005
        is_alias = !is_alias;
×
1006
        break;
×
1007
      case '|':
6✔
1008
        if (!pat_or)
6✔
1009
        {
1010
          if (!curlist)
6✔
1011
          {
1012
            buf_printf(err, _("error in pattern at: %s"), ps->dptr);
1✔
1013
            buf_pool_release(&ps);
1✔
1014
            return NULL;
1✔
1015
          }
1016

1017
          struct Pattern *pat = SLIST_FIRST(curlist);
5✔
1018
          if (SLIST_NEXT(pat, entries))
5✔
1019
          {
1020
            /* A & B | C == (A & B) | C */
1021
            struct Pattern *root = attach_new_root(&curlist);
1022
            root->op = MUTT_PAT_AND;
2✔
1023
          }
1024

1025
          pat_or = true;
1026
        }
1027
        ps->dptr++;
5✔
1028
        implicit = false;
1029
        pat_not = false;
1030
        all_addr = false;
1031
        is_alias = false;
1032
        break;
5✔
1033
      case '%':
112✔
1034
      case '=':
1035
      case '~':
1036
      {
1037
        if (ps->dptr[1] == '\0')
112✔
1038
        {
1039
          buf_printf(err, _("missing pattern: %s"), ps->dptr);
×
1040
          goto cleanup;
×
1041
        }
1042
        short thread_op = 0;
1043
        if (ps->dptr[1] == '(')
112✔
1044
          thread_op = MUTT_PAT_THREAD;
1045
        else if ((ps->dptr[1] == '<') && (ps->dptr[2] == '('))
111✔
1046
          thread_op = MUTT_PAT_PARENT;
1047
        else if ((ps->dptr[1] == '>') && (ps->dptr[2] == '('))
110✔
1048
          thread_op = MUTT_PAT_CHILDREN;
1049
        if (thread_op != 0)
1050
        {
1051
          ps->dptr++; /* skip ~ */
3✔
1052
          if ((thread_op == MUTT_PAT_PARENT) || (thread_op == MUTT_PAT_CHILDREN))
3✔
1053
            ps->dptr++;
2✔
1054
          p = find_matching_paren(ps->dptr + 1);
3✔
1055
          if (p[0] != ')')
3✔
1056
          {
1057
            buf_printf(err, _("mismatched parentheses: %s"), ps->dptr);
×
1058
            goto cleanup;
×
1059
          }
1060
          struct Pattern *leaf = attach_new_leaf(&curlist);
3✔
1061
          leaf->op = thread_op;
3✔
1062
          leaf->pat_not = pat_not;
3✔
1063
          leaf->all_addr = all_addr;
3✔
1064
          leaf->is_alias = is_alias;
3✔
1065
          pat_not = false;
1066
          all_addr = false;
1067
          is_alias = false;
1068
          /* compile the sub-expression */
1069
          buf = mutt_strn_dup(ps->dptr + 1, p - (ps->dptr + 1));
3✔
1070
          leaf->child = mutt_pattern_comp(mv, buf, flags, err);
3✔
1071
          if (!leaf->child)
3✔
1072
          {
1073
            FREE(&buf);
×
1074
            goto cleanup;
×
1075
          }
1076
          FREE(&buf);
3✔
1077
          ps->dptr = p + 1; /* restore location */
3✔
1078
          break;
3✔
1079
        }
1080
        if (implicit && pat_or)
109✔
1081
        {
1082
          /* A | B & C == (A | B) & C */
1083
          struct Pattern *root = attach_new_root(&curlist);
1084
          root->op = MUTT_PAT_OR;
1✔
1085
          pat_or = false;
1086
        }
1087

1088
        char prefix = ps->dptr[0];
109✔
1089
        entry = lookup_tag(prefix, ps->dptr[1]);
109✔
1090
        if (!entry)
109✔
1091
        {
1092
          buf_printf(err, _("%c%c: invalid pattern"), prefix, ps->dptr[1]);
×
1093
          goto cleanup;
×
1094
        }
1095
        if (entry->flags && ((flags & entry->flags) == 0))
109✔
1096
        {
1097
          buf_printf(err, _("%c%c: not supported in this mode"), prefix, ps->dptr[1]);
4✔
1098
          goto cleanup;
4✔
1099
        }
1100

1101
        struct Pattern *leaf = attach_new_leaf(&curlist);
105✔
1102
        leaf->pat_not = pat_not;
105✔
1103
        leaf->all_addr = all_addr;
105✔
1104
        leaf->is_alias = is_alias;
105✔
1105
        leaf->sendmode = (flags & MUTT_PC_SEND_MODE_SEARCH);
105✔
1106
        leaf->op = entry->op;
105✔
1107
        pat_not = false;
1108
        all_addr = false;
1109
        is_alias = false;
1110

1111
        // Determine the actual eat_arg to use.
1112
        // If the entry was found via fallback (entry->prefix is '~' but we used '=' or '%'),
1113
        // override the eat_arg to use string or group parsing respectively.
1114
        enum PatternEat eat_arg = entry->eat_arg;
105✔
1115
        if ((entry->prefix == '~') && (prefix == '=') && (eat_arg == EAT_REGEX))
105✔
1116
          eat_arg = EAT_STRING;
1117
        else if ((entry->prefix == '~') && (prefix == '%') && (eat_arg == EAT_REGEX))
90✔
1118
          eat_arg = EAT_GROUP;
1119

1120
        ps->dptr++; /* move past the prefix (~, %, =) */
105✔
1121
        ps->dptr++; /* eat the operator and any optional whitespace */
105✔
1122
        SKIPWS(ps->dptr);
173✔
1123
        if (eat_arg)
105✔
1124
        {
1125
          if (ps->dptr[0] == '\0')
64✔
1126
          {
1127
            buf_addstr(err, _("missing parameter"));
1✔
1128
            goto cleanup;
1✔
1129
          }
1130
          switch (eat_arg)
63✔
1131
          {
1132
            case EAT_REGEX:
23✔
1133
              if (!eat_regex(leaf, flags, ps, err))
23✔
1134
                goto cleanup;
×
1135
              break;
1136
            case EAT_STRING:
14✔
1137
              if (!eat_string(leaf, flags, ps, err))
14✔
1138
                goto cleanup;
×
1139
              break;
1140
            case EAT_GROUP:
×
1141
              if (!eat_group(leaf, flags, ps, err))
×
1142
                goto cleanup;
×
1143
              break;
1144
            case EAT_DATE:
20✔
1145
              if (!eat_date(leaf, flags, ps, err))
20✔
1146
                goto cleanup;
4✔
1147
              break;
1148
            case EAT_RANGE:
3✔
1149
              if (!eat_range(leaf, flags, ps, err))
3✔
1150
                goto cleanup;
×
1151
              break;
1152
            case EAT_MESSAGE_RANGE:
2✔
1153
              if (!eat_message_range(leaf, flags, ps, err, mv))
2✔
1154
                goto cleanup;
2✔
1155
              break;
1156
            case EAT_QUERY:
1✔
1157
              if (!eat_query(leaf, flags, ps, err, m))
1✔
1158
                goto cleanup;
1✔
1159
              break;
1160
            default:
1161
              break;
1162
          }
1163
        }
1164
        implicit = true;
1165
        break;
1166
      }
1167

1168
      case '(':
5✔
1169
      {
1170
        p = find_matching_paren(ps->dptr + 1);
5✔
1171
        if (p[0] != ')')
5✔
1172
        {
1173
          buf_printf(err, _("mismatched parentheses: %s"), ps->dptr);
×
1174
          goto cleanup;
×
1175
        }
1176
        /* compile the sub-expression */
1177
        buf = mutt_strn_dup(ps->dptr + 1, p - (ps->dptr + 1));
5✔
1178
        struct PatternList *sub = mutt_pattern_comp(mv, buf, flags, err);
5✔
1179
        FREE(&buf);
5✔
1180
        if (!sub)
5✔
1181
          goto cleanup;
×
1182
        struct Pattern *leaf = SLIST_FIRST(sub);
5✔
1183
        if (curlist)
5✔
1184
        {
1185
          attach_leaf(curlist, leaf);
1186
          FREE(&sub);
2✔
1187
        }
1188
        else
1189
        {
1190
          curlist = sub;
3✔
1191
        }
1192
        leaf->pat_not ^= pat_not;
5✔
1193
        leaf->all_addr |= all_addr;
5✔
1194
        leaf->is_alias |= is_alias;
5✔
1195
        pat_not = false;
1196
        all_addr = false;
1197
        is_alias = false;
1198
        ps->dptr = p + 1; /* restore location */
5✔
1199
        break;
5✔
1200
      }
1201

1202
      default:
1✔
1203
        buf_printf(err, _("error in pattern at: %s"), ps->dptr);
1✔
1204
        goto cleanup;
1✔
1205
    }
1206
    SKIPWS(ps->dptr);
121✔
1207
  }
1208
  buf_pool_release(&ps);
89✔
1209

1210
  if (!curlist)
89✔
1211
  {
1212
    buf_strcpy(err, _("empty pattern"));
×
1213
    return NULL;
×
1214
  }
1215

1216
  if (SLIST_NEXT(SLIST_FIRST(curlist), entries))
89✔
1217
  {
1218
    struct Pattern *root = attach_new_root(&curlist);
1219
    root->op = pat_or ? MUTT_PAT_OR : MUTT_PAT_AND;
20✔
1220
  }
1221

1222
  return curlist;
89✔
1223

1224
cleanup:
13✔
1225
  mutt_pattern_free(&curlist);
13✔
1226
  buf_pool_release(&ps);
13✔
1227
  return NULL;
13✔
1228
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc