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

neomutt / neomutt / 21576993800

02 Feb 2026 01:15AM UTC coverage: 42.169% (+0.2%) from 42.019%
21576993800

push

github

flatcap
build: force all-docs before validation

11846 of 28092 relevant lines covered (42.17%)

447.19 hits per line

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

77.19
/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 "gui/lib.h"
46
#include "lib.h"
47
#include "parse/lib.h"
48

49
// clang-format off
50
typedef uint16_t ParseDateRangeFlags; ///< Flags for parse_date_range(), e.g. #MUTT_PDR_MINUS
51
#define MUTT_PDR_NO_FLAGS       0  ///< No flags are set
52
#define MUTT_PDR_MINUS    (1 << 0) ///< Pattern contains a range
53
#define MUTT_PDR_PLUS     (1 << 1) ///< Extend the range using '+'
54
#define MUTT_PDR_WINDOW   (1 << 2) ///< Extend the range in both directions using '*'
55
#define MUTT_PDR_ABSOLUTE (1 << 3) ///< Absolute pattern range
56
#define MUTT_PDR_DONE     (1 << 4) ///< Pattern parse successfully
57
#define MUTT_PDR_ERROR    (1 << 8) ///< Invalid pattern
58
// clang-format on
59

60
#define MUTT_PDR_ERRORDONE (MUTT_PDR_ERROR | MUTT_PDR_DONE)
61

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

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

98
  rc = true;
99

100
out:
23✔
101
  buf_pool_release(&token);
23✔
102
  return rc;
23✔
103
}
104

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

126
  pat->string_match = true;
14✔
127
  pat->p.str = buf_strdup(token);
14✔
128
  pat->ign_case = mutt_mb_is_lower(token->data);
14✔
129

130
  rc = true;
131

132
out:
14✔
133
  buf_pool_release(&token);
14✔
134
  return rc;
14✔
135
}
136

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

158
  pat->group_match = true;
×
159
  pat->p.group = groups_get_group(NeoMutt->groups, token->data);
×
160

161
  rc = true;
162

163
out:
×
164
  buf_pool_release(&token);
×
165
  return rc;
×
166
}
167

168
/**
169
 * add_query_msgid - Parse a Message-Id and add it to a list - Implements ::mutt_file_map_t - @ingroup mutt_file_map_api
170
 * @retval true Always
171
 */
172
static bool add_query_msgid(char *line, int line_num, void *user_data)
×
173
{
174
  struct ListHead *msgid_list = (struct ListHead *) (user_data);
175
  char *nows = mutt_str_skip_whitespace(line);
×
176
  if (*nows == '\0')
×
177
    return true;
178
  mutt_str_remove_trailing_ws(nows);
×
179
  mutt_list_insert_tail(msgid_list, mutt_str_dup(nows));
×
180
  return true;
×
181
}
182

183
/**
184
 * eat_query - Parse a query for an external search program - Implements ::eat_arg_t - @ingroup eat_arg_api
185
 * @param pat   Pattern to store the results in
186
 * @param flags Flags, e.g. #MUTT_PC_PATTERN_DYNAMIC
187
 * @param s     String to parse
188
 * @param err   Buffer for error messages
189
 * @param m     Mailbox
190
 * @retval true The pattern was read successfully
191
 */
192
static bool eat_query(struct Pattern *pat, PatternCompFlags flags,
1✔
193
                      struct Buffer *s, struct Buffer *err, struct Mailbox *m)
194
{
195
  struct Buffer *cmd = buf_pool_get();
1✔
196
  struct Buffer *tok = buf_pool_get();
1✔
197
  bool rc = false;
198

199
  FILE *fp = NULL;
1✔
200

201
  const char *const c_external_search_command = cs_subset_string(NeoMutt->sub, "external_search_command");
1✔
202
  if (!c_external_search_command)
1✔
203
  {
204
    buf_addstr(err, _("No search command defined"));
×
205
    goto out;
×
206
  }
207

208
  char *pexpr = s->dptr;
1✔
209
  if ((parse_extract_token(tok, s, TOKEN_PATTERN | TOKEN_COMMENT) != 0) || !tok->data)
1✔
210
  {
211
    buf_printf(err, _("Error in expression: %s"), pexpr);
×
212
    goto out;
×
213
  }
214
  if (*tok->data == '\0')
1✔
215
  {
216
    buf_addstr(err, _("Empty expression"));
×
217
    goto out;
×
218
  }
219

220
  buf_addstr(cmd, c_external_search_command);
1✔
221
  buf_addch(cmd, ' ');
1✔
222

223
  if (m)
1✔
224
  {
225
    char *escaped_folder = mutt_path_escape(mailbox_path(m));
×
226
    mutt_debug(LL_DEBUG2, "escaped folder path: %s\n", escaped_folder);
×
227
    buf_addch(cmd, '\'');
×
228
    buf_addstr(cmd, escaped_folder);
×
229
    buf_addch(cmd, '\'');
×
230
  }
231
  else
232
  {
233
    buf_addch(cmd, '/');
1✔
234
  }
235
  buf_addch(cmd, ' ');
1✔
236
  buf_addstr(cmd, tok->data);
1✔
237

238
  mutt_message(_("Running search command: %s ..."), cmd->data);
1✔
239
  pat->is_multi = true;
1✔
240
  mutt_list_clear(&pat->p.multi_cases);
1✔
241
  pid_t pid = filter_create(cmd->data, NULL, &fp, NULL, NeoMutt->env);
1✔
242
  if (pid < 0)
1✔
243
  {
244
    buf_printf(err, "unable to fork command: %s\n", cmd->data);
×
245
    goto out;
×
246
  }
247

248
  mutt_file_map_lines(add_query_msgid, &pat->p.multi_cases, fp, MUTT_RL_NO_FLAGS);
1✔
249
  mutt_file_fclose(&fp);
1✔
250
  filter_wait(pid);
1✔
251

252
  rc = true;
253

254
out:
1✔
255
  buf_pool_release(&cmd);
1✔
256
  buf_pool_release(&tok);
1✔
257
  return rc;
1✔
258
}
259

260
/**
261
 * get_offset - Calculate a symbolic offset
262
 * @param tm   Store the time here
263
 * @param s    string to parse
264
 * @param sign Sign of range, 1 for positive, -1 for negative
265
 * @retval ptr Next char after parsed offset
266
 *
267
 * - Ny years
268
 * - Nm months
269
 * - Nw weeks
270
 * - Nd days
271
 */
272
static const char *get_offset(struct tm *tm, const char *s, int sign)
16✔
273
{
274
  char *ps = NULL;
16✔
275
  int offset = strtol(s, &ps, 0);
16✔
276
  if (((sign < 0) && (offset > 0)) || ((sign > 0) && (offset < 0)))
16✔
277
    offset = -offset;
14✔
278

279
  switch (*ps)
16✔
280
  {
281
    case 'y':
2✔
282
      tm->tm_year += offset;
2✔
283
      break;
2✔
284
    case 'm':
2✔
285
      tm->tm_mon += offset;
2✔
286
      break;
2✔
287
    case 'w':
1✔
288
      tm->tm_mday += 7 * offset;
1✔
289
      break;
1✔
290
    case 'd':
6✔
291
      tm->tm_mday += offset;
6✔
292
      break;
6✔
293
    case 'H':
1✔
294
      tm->tm_hour += offset;
1✔
295
      break;
1✔
296
    case 'M':
1✔
297
      tm->tm_min += offset;
1✔
298
      break;
1✔
299
    case 'S':
1✔
300
      tm->tm_sec += offset;
1✔
301
      break;
1✔
302
    default:
303
      return s;
304
  }
305
  mutt_date_normalize_time(tm);
14✔
306
  return ps + 1;
14✔
307
}
308

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

332
  for (int v = 0; v < 8; v++)
51✔
333
  {
334
    if (s[v] && (s[v] >= '0') && (s[v] <= '9'))
48✔
335
      continue;
336

337
    iso8601 = false;
338
    break;
339
  }
340

341
  if (iso8601)
11✔
342
  {
343
    int year = 0;
3✔
344
    int month = 0;
3✔
345
    int mday = 0;
3✔
346
    sscanf(s, "%4d%2d%2d", &year, &month, &mday);
3✔
347

348
    t->tm_year = year;
3✔
349
    if (t->tm_year > 1900)
3✔
350
      t->tm_year -= 1900;
3✔
351
    t->tm_mon = month - 1;
3✔
352
    t->tm_mday = mday;
3✔
353

354
    if ((t->tm_mday < 1) || (t->tm_mday > 31))
3✔
355
    {
356
      buf_printf(err, _("Invalid day of month: %s"), s);
1✔
357
      return NULL;
1✔
358
    }
359
    if ((t->tm_mon < 0) || (t->tm_mon > 11))
2✔
360
    {
361
      buf_printf(err, _("Invalid month: %s"), s);
1✔
362
      return NULL;
1✔
363
    }
364

365
    return (s + 8);
1✔
366
  }
367

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

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

501
/**
502
 * adjust_date_range - Put a date range in the correct order
503
 * @param[in,out] min Earlier date
504
 * @param[in,out] max Later date
505
 */
506
static void adjust_date_range(struct tm *min, struct tm *max)
16✔
507
{
508
  if ((min->tm_year > max->tm_year) ||
16✔
509
      ((min->tm_year == max->tm_year) && (min->tm_mon > max->tm_mon)) ||
2✔
510
      ((min->tm_year == max->tm_year) && (min->tm_mon == max->tm_mon) &&
15✔
511
       (min->tm_mday > max->tm_mday)))
2✔
512
  {
513
    int tmp;
514

515
    tmp = min->tm_year;
516
    min->tm_year = max->tm_year;
1✔
517
    max->tm_year = tmp;
1✔
518

519
    tmp = min->tm_mon;
1✔
520
    min->tm_mon = max->tm_mon;
1✔
521
    max->tm_mon = tmp;
1✔
522

523
    tmp = min->tm_mday;
1✔
524
    min->tm_mday = max->tm_mday;
1✔
525
    max->tm_mday = tmp;
1✔
526

527
    min->tm_hour = 0;
1✔
528
    min->tm_min = 0;
1✔
529
    min->tm_sec = 0;
1✔
530
    max->tm_hour = 23;
1✔
531
    max->tm_min = 59;
1✔
532
    max->tm_sec = 59;
1✔
533
  }
534
}
16✔
535

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

552
  /* Arbitrary year in the future.  Don't set this too high or
553
   * mutt_date_make_time() returns something larger than will fit in a time_t
554
   * on some systems */
555
  struct tm max = { 0 };
20✔
556
  max.tm_year = 130;
20✔
557
  max.tm_mon = 11;
20✔
558
  max.tm_mday = 31;
20✔
559
  max.tm_hour = 23;
20✔
560
  max.tm_min = 59;
20✔
561
  max.tm_sec = 59;
20✔
562

563
  if (strchr("<>=", s[0]))
20✔
564
  {
565
    /* offset from current time
566
     *  <3d  less than three days ago
567
     *  >3d  more than three days ago
568
     *  =3d  exactly three days ago */
569
    struct tm *tm = NULL;
570
    bool exact = false;
571

572
    if (s[0] == '<')
11✔
573
    {
574
      min = mutt_date_localtime(mutt_date_now());
11✔
575
      tm = &min;
576
    }
577
    else
578
    {
579
      max = mutt_date_localtime(mutt_date_now());
×
580
      tm = &max;
581

582
      if (s[0] == '=')
×
583
        exact = true;
584
    }
585

586
    /* Reset the HMS unless we are relative matching using one of those
587
     * offsets. */
588
    char *offset_type = NULL;
11✔
589
    strtol(s + 1, &offset_type, 0);
11✔
590
    if (!(*offset_type && strchr("HMS", *offset_type)))
11✔
591
    {
592
      tm->tm_hour = 23;
8✔
593
      tm->tm_min = 59;
8✔
594
      tm->tm_sec = 59;
8✔
595
    }
596

597
    /* force negative offset */
598
    get_offset(tm, s + 1, -1);
11✔
599

600
    if (exact)
11✔
601
    {
602
      /* start at the beginning of the day in question */
603
      memcpy(&min, &max, sizeof(max));
604
      min.tm_hour = 0;
×
605
      min.tm_sec = 0;
×
606
      min.tm_min = 0;
×
607
    }
608
  }
609
  else
610
  {
611
    const char *pc = s;
612

613
    bool have_min = false;
614
    bool until_now = false;
615
    if (mutt_isdigit(*pc))
9✔
616
    {
617
      /* minimum date specified */
618
      pc = get_date(pc, &min, err);
9✔
619
      if (!pc)
9✔
620
      {
621
        return false;
622
      }
623
      have_min = true;
624
      SKIPWS(pc);
5✔
625
      if (*pc == '-')
5✔
626
      {
627
        const char *pt = pc + 1;
2✔
628
        SKIPWS(pt);
2✔
629
        until_now = (*pt == '\0');
2✔
630
      }
631
    }
632

633
    if (!until_now)
5✔
634
    { /* max date or relative range/window */
635

636
      struct tm base_min = { 0 };
5✔
637

638
      if (!have_min)
5✔
639
      { /* save base minimum and set current date, e.g. for "-3d+1d" */
640
        memcpy(&base_min, &min, sizeof(base_min));
641
        min = mutt_date_localtime(mutt_date_now());
×
642
        min.tm_hour = 0;
×
643
        min.tm_sec = 0;
×
644
        min.tm_min = 0;
×
645
      }
646

647
      /* preset max date for relative offsets,
648
       * if nothing follows we search for messages on a specific day */
649
      max.tm_year = min.tm_year;
5✔
650
      max.tm_mon = min.tm_mon;
5✔
651
      max.tm_mday = min.tm_mday;
5✔
652

653
      if (!parse_date_range(pc, &min, &max, have_min, &base_min, err))
5✔
654
      { /* bail out on any parsing error */
655
        return false;
×
656
      }
657
    }
658
  }
659

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

663
  pat->min = mutt_date_make_time(&min, true);
16✔
664
  pat->max = mutt_date_make_time(&max, true);
16✔
665

666
  return true;
16✔
667
}
668

669
/**
670
 * eat_range - Parse a number range - Implements ::eat_arg_t - @ingroup eat_arg_api
671
 */
672
static bool eat_range(struct Pattern *pat, PatternCompFlags flags,
3✔
673
                      struct Buffer *s, struct Buffer *err)
674
{
675
  char *tmp = NULL;
3✔
676
  bool do_exclusive = false;
677
  bool skip_quote = false;
678

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

730
  if (mutt_isdigit(*tmp))
2✔
731
  {
732
    /* range maximum */
733
    pat->max = strtol(tmp, &tmp, 0);
2✔
734
    if (mutt_toupper(*tmp) == 'K')
2✔
735
    {
736
      pat->max *= 1024;
1✔
737
      tmp++;
1✔
738
    }
739
    else if (mutt_toupper(*tmp) == 'M')
1✔
740
    {
741
      pat->max *= 1048576;
×
742
      tmp++;
×
743
    }
744
    if (do_exclusive)
2✔
745
      (pat->max)--;
1✔
746
  }
747
  else
748
  {
749
    pat->max = MUTT_MAXRANGE;
×
750
  }
751

752
  if (skip_quote && (*tmp == '"'))
2✔
753
    tmp++;
×
754

755
  SKIPWS(tmp);
2✔
756
  s->dptr = tmp;
2✔
757
  return true;
2✔
758
}
759

760
/**
761
 * eat_date - Parse a date pattern - Implements ::eat_arg_t - @ingroup eat_arg_api
762
 */
763
static bool eat_date(struct Pattern *pat, PatternCompFlags flags,
20✔
764
                     struct Buffer *s, struct Buffer *err)
765
{
766
  struct Buffer *tmp = buf_pool_get();
20✔
767
  bool rc = false;
768

769
  char *pexpr = s->dptr;
20✔
770
  if (parse_extract_token(tmp, s, TOKEN_COMMENT | TOKEN_PATTERN) != 0)
20✔
771
  {
772
    buf_printf(err, _("Error in expression: %s"), pexpr);
×
773
    goto out;
×
774
  }
775

776
  if (buf_is_empty(tmp))
20✔
777
  {
778
    buf_addstr(err, _("Empty expression"));
×
779
    goto out;
×
780
  }
781

782
  if (flags & MUTT_PC_PATTERN_DYNAMIC)
20✔
783
  {
784
    pat->dynamic = true;
3✔
785
    pat->p.str = buf_strdup(tmp);
3✔
786
  }
787

788
  rc = eval_date_minmax(pat, tmp->data, err);
20✔
789

790
out:
20✔
791
  buf_pool_release(&tmp);
20✔
792
  return rc;
20✔
793
}
794

795
/**
796
 * find_matching_paren - Find the matching parenthesis
797
 * @param s string to search
798
 * @retval ptr
799
 * - Matching close parenthesis
800
 * - End of string NUL, if not found
801
 */
802
static /* const */ char *find_matching_paren(/* const */ char *s)
8✔
803
{
804
  int level = 1;
805

806
  for (; *s; s++)
78✔
807
  {
808
    if (*s == '(')
78✔
809
    {
810
      level++;
×
811
    }
812
    else if (*s == ')')
78✔
813
    {
814
      level--;
8✔
815
      if (level == 0)
8✔
816
        break;
817
    }
818
  }
819
  return s;
8✔
820
}
821

822
/**
823
 * mutt_pattern_free - Free a Pattern
824
 * @param[out] pat Pattern to free
825
 */
826
void mutt_pattern_free(struct PatternList **pat)
192✔
827
{
828
  if (!pat || !*pat)
192✔
829
    return;
105✔
830

831
  struct Pattern *np = SLIST_FIRST(*pat);
87✔
832
  struct Pattern *next = NULL;
833

834
  while (np)
187✔
835
  {
836
    next = SLIST_NEXT(np, entries);
100✔
837

838
    if (np->is_multi)
100✔
839
    {
840
      mutt_list_free(&np->p.multi_cases);
1✔
841
    }
842
    else if (np->string_match || np->dynamic)
99✔
843
    {
844
      FREE(&np->p.str);
14✔
845
    }
846
    else if (np->group_match)
85✔
847
    {
848
      np->p.group = NULL;
×
849
    }
850
    else if (np->p.regex)
85✔
851
    {
852
      regfree(np->p.regex);
13✔
853
      FREE(&np->p.regex);
13✔
854
    }
855

856
#ifdef USE_DEBUG_GRAPHVIZ
857
    FREE(&np->raw_pattern);
858
#endif
859
    mutt_pattern_free(&np->child);
100✔
860
    FREE(&np);
100✔
861

862
    np = next;
100✔
863
  }
864

865
  FREE(pat);
87✔
866
}
867

868
/**
869
 * mutt_pattern_new - Create a new Pattern
870
 * @retval ptr Newly created Pattern
871
 */
872
static struct Pattern *mutt_pattern_new(void)
873
{
874
  return MUTT_MEM_CALLOC(1, struct Pattern);
14✔
875
}
876

877
/**
878
 * mutt_pattern_list_new - Create a new list containing a Pattern
879
 * @retval ptr Newly created list containing a single node with a Pattern
880
 */
881
static struct PatternList *mutt_pattern_list_new(void)
109✔
882
{
883
  struct PatternList *h = MUTT_MEM_CALLOC(1, struct PatternList);
109✔
884
  SLIST_INIT(h);
109✔
885
  struct Pattern *p = mutt_pattern_new();
886
  SLIST_INSERT_HEAD(h, p, entries);
109✔
887
  return h;
109✔
888
}
889

890
/**
891
 * attach_leaf - Attach a Pattern to a Pattern List
892
 * @param list Pattern List to attach to
893
 * @param leaf Pattern to attach
894
 * @retval ptr Attached leaf
895
 */
896
static struct Pattern *attach_leaf(struct PatternList *list, struct Pattern *leaf)
897
{
898
  struct Pattern *last = NULL;
899
  SLIST_FOREACH(last, list, entries)
17✔
900
  {
901
    // TODO - or we could use a doubly-linked list
902
    if (!SLIST_NEXT(last, entries))
17✔
903
    {
904
      SLIST_NEXT(last, entries) = leaf;
16✔
905
      break;
16✔
906
    }
907
  }
908
  return leaf;
909
}
910

911
/**
912
 * attach_new_root - Create a new Pattern as a parent for a List
913
 * @param curlist Pattern List
914
 * @retval ptr First Pattern in the original List
915
 *
916
 * @note curlist will be altered to the new root Pattern
917
 */
918
static struct Pattern *attach_new_root(struct PatternList **curlist)
919
{
920
  struct PatternList *root = mutt_pattern_list_new();
109✔
921
  struct Pattern *leaf = SLIST_FIRST(root);
109✔
922
  leaf->child = *curlist;
109✔
923
  *curlist = root;
109✔
924
  return leaf;
925
}
926

927
/**
928
 * attach_new_leaf - Attach a new Pattern to a List
929
 * @param curlist Pattern List
930
 * @retval ptr New Pattern in the original List
931
 *
932
 * @note curlist may be altered
933
 */
934
static struct Pattern *attach_new_leaf(struct PatternList **curlist)
108✔
935
{
936
  if (*curlist)
108✔
937
  {
938
    return attach_leaf(*curlist, mutt_pattern_new());
28✔
939
  }
940
  else
941
  {
942
    return attach_new_root(curlist);
94✔
943
  }
944
}
945

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

970
  if (!s || (s[0] == '\0'))
104✔
971
  {
972
    buf_strcpy(err, _("empty pattern"));
1✔
973
    return NULL;
1✔
974
  }
975

976
  struct Buffer *ps = buf_pool_get();
103✔
977
  buf_strcpy(ps, s);
103✔
978
  buf_seek(ps, 0);
103✔
979

980
  SKIPWS(ps->dptr);
103✔
981
  while (*ps->dptr)
218✔
982
  {
983
    switch (*ps->dptr)
128✔
984
    {
985
      case '^':
×
986
        ps->dptr++;
×
987
        all_addr = !all_addr;
×
988
        break;
×
989
      case '!':
4✔
990
        ps->dptr++;
4✔
991
        pat_not = !pat_not;
4✔
992
        break;
4✔
993
      case '@':
×
994
        ps->dptr++;
×
995
        is_alias = !is_alias;
×
996
        break;
×
997
      case '|':
6✔
998
        if (!pat_or)
6✔
999
        {
1000
          if (!curlist)
6✔
1001
          {
1002
            buf_printf(err, _("error in pattern at: %s"), ps->dptr);
1✔
1003
            buf_pool_release(&ps);
1✔
1004
            return NULL;
1✔
1005
          }
1006

1007
          struct Pattern *pat = SLIST_FIRST(curlist);
5✔
1008
          if (SLIST_NEXT(pat, entries))
5✔
1009
          {
1010
            /* A & B | C == (A & B) | C */
1011
            struct Pattern *root = attach_new_root(&curlist);
1012
            root->op = MUTT_PAT_AND;
2✔
1013
          }
1014

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

1078
        char prefix = ps->dptr[0];
109✔
1079
        entry = lookup_tag(prefix, ps->dptr[1]);
109✔
1080
        if (!entry)
109✔
1081
        {
1082
          buf_printf(err, _("%c%c: invalid pattern"), prefix, ps->dptr[1]);
×
1083
          goto cleanup;
×
1084
        }
1085
        if (entry->flags && ((flags & entry->flags) == 0))
109✔
1086
        {
1087
          buf_printf(err, _("%c%c: not supported in this mode"), prefix, ps->dptr[1]);
4✔
1088
          goto cleanup;
4✔
1089
        }
1090

1091
        struct Pattern *leaf = attach_new_leaf(&curlist);
105✔
1092
        leaf->pat_not = pat_not;
105✔
1093
        leaf->all_addr = all_addr;
105✔
1094
        leaf->is_alias = is_alias;
105✔
1095
        leaf->sendmode = (flags & MUTT_PC_SEND_MODE_SEARCH);
105✔
1096
        leaf->op = entry->op;
105✔
1097
        pat_not = false;
1098
        all_addr = false;
1099
        is_alias = false;
1100

1101
        // Determine the actual eat_arg to use.
1102
        // If the entry was found via fallback (entry->prefix is '~' but we used '=' or '%'),
1103
        // override the eat_arg to use string or group parsing respectively.
1104
        enum PatternEat eat_arg = entry->eat_arg;
105✔
1105
        if ((entry->prefix == '~') && (prefix == '=') && (eat_arg == EAT_REGEX))
105✔
1106
          eat_arg = EAT_STRING;
1107
        else if ((entry->prefix == '~') && (prefix == '%') && (eat_arg == EAT_REGEX))
90✔
1108
          eat_arg = EAT_GROUP;
1109

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

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

1192
      default:
1✔
1193
        buf_printf(err, _("error in pattern at: %s"), ps->dptr);
1✔
1194
        goto cleanup;
1✔
1195
    }
1196
    SKIPWS(ps->dptr);
122✔
1197
  }
1198
  buf_pool_release(&ps);
90✔
1199

1200
  if (!curlist)
90✔
1201
  {
1202
    buf_strcpy(err, _("empty pattern"));
×
1203
    return NULL;
×
1204
  }
1205

1206
  if (SLIST_NEXT(SLIST_FIRST(curlist), entries))
90✔
1207
  {
1208
    struct Pattern *root = attach_new_root(&curlist);
1209
    root->op = pat_or ? MUTT_PAT_OR : MUTT_PAT_AND;
20✔
1210
  }
1211

1212
  return curlist;
90✔
1213

1214
cleanup:
12✔
1215
  mutt_pattern_free(&curlist);
12✔
1216
  buf_pool_release(&ps);
12✔
1217
  return NULL;
12✔
1218
}
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