Coverage Report

Created: 2026-10-17 06:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/pigeonhole/src/lib-sieve/plugins/editheader/cmd-deleteheader.c
Line
Count
Source
1
/* Copyright (c) Pigeonhole authors, see top-level COPYING file */
2
3
#include "lib.h"
4
#include "str-sanitize.h"
5
#include "mail-storage.h"
6
7
#include "rfc2822.h"
8
#include "edit-mail.h"
9
10
#include "sieve-common.h"
11
#include "sieve-commands.h"
12
#include "sieve-code.h"
13
#include "sieve-message.h"
14
#include "sieve-comparators.h"
15
#include "sieve-match-types.h"
16
17
#include "sieve-validator.h"
18
#include "sieve-generator.h"
19
#include "sieve-interpreter.h"
20
#include "sieve-dump.h"
21
#include "sieve-match.h"
22
23
#include "ext-editheader-common.h"
24
25
/*
26
 * Deleteheader command
27
 *
28
 * Syntax:
29
 *   deleteheader [":index" <fieldno: number> [":last"]]
30
 *                [COMPARATOR] [MATCH-TYPE]
31
 *                <field-name: string> [<value-patterns: string-list>]
32
 */
33
34
static bool
35
cmd_deleteheader_registered(struct sieve_validator *valdtr,
36
          const struct sieve_extension *ext,
37
          struct sieve_command_registration *cmd_reg);
38
static bool
39
cmd_deleteheader_validate(struct sieve_validator *valdtr,
40
        struct sieve_command *cmd);
41
static bool
42
cmd_deleteheader_generate(const struct sieve_codegen_env *cgenv,
43
        struct sieve_command *cmd);
44
45
const struct sieve_command_def deleteheader_command = {
46
  .identifier = "deleteheader",
47
  .type = SCT_COMMAND,
48
  .positional_args = -1, /* We check positional arguments ourselves */
49
  .subtests = 0,
50
  .block_allowed = FALSE,
51
  .block_required = FALSE,
52
  .registered = cmd_deleteheader_registered,
53
  .validate = cmd_deleteheader_validate,
54
  .generate = cmd_deleteheader_generate,
55
};
56
57
/*
58
 * Deleteheader command tags
59
 */
60
61
/* Forward declarations */
62
63
static bool
64
cmd_deleteheader_validate_index_tag(struct sieve_validator *valdtr,
65
            struct sieve_ast_argument **arg,
66
            struct sieve_command *cmd);
67
static bool
68
cmd_deleteheader_validate_last_tag(struct sieve_validator *valdtr,
69
           struct sieve_ast_argument **arg,
70
           struct sieve_command *cmd);
71
72
/* Argument objects */
73
74
static const struct sieve_argument_def deleteheader_index_tag = {
75
  .identifier = "index",
76
  .validate = cmd_deleteheader_validate_index_tag,
77
};
78
79
static const struct sieve_argument_def deleteheader_last_tag = {
80
  .identifier = "last",
81
  .validate = cmd_deleteheader_validate_last_tag,
82
};
83
84
/* Codes for optional arguments */
85
86
enum cmd_deleteheader_optional {
87
  OPT_INDEX = SIEVE_MATCH_OPT_LAST,
88
  OPT_LAST,
89
};
90
91
/*
92
 * Deleteheader operation
93
 */
94
95
static bool
96
cmd_deleteheader_operation_dump(const struct sieve_dumptime_env *denv,
97
        sieve_size_t *address);
98
static int
99
cmd_deleteheader_operation_execute(const struct sieve_runtime_env *renv,
100
           sieve_size_t *address);
101
102
const struct sieve_operation_def deleteheader_operation = {
103
  .mnemonic = "DELETEHEADER",
104
  .ext_def = &editheader_extension,
105
  .code = EXT_EDITHEADER_OPERATION_DELETEHEADER,
106
  .dump = cmd_deleteheader_operation_dump,
107
  .execute = cmd_deleteheader_operation_execute,
108
};
109
110
/*
111
 * Command registration
112
 */
113
114
static bool
115
cmd_deleteheader_registered(struct sieve_validator *valdtr,
116
          const struct sieve_extension *ext ATTR_UNUSED,
117
          struct sieve_command_registration *cmd_reg)
118
0
{
119
  /* The order of these is not significant */
120
0
  sieve_comparators_link_tag(valdtr, cmd_reg,
121
0
           SIEVE_MATCH_OPT_COMPARATOR);
122
0
  sieve_match_types_link_tags(valdtr, cmd_reg,
123
0
            SIEVE_MATCH_OPT_MATCH_TYPE);
124
125
0
  sieve_validator_register_tag(valdtr, cmd_reg, ext,
126
0
             &deleteheader_index_tag, OPT_INDEX);
127
0
  sieve_validator_register_tag(valdtr, cmd_reg, ext,
128
0
             &deleteheader_last_tag, OPT_LAST);
129
130
0
  return TRUE;
131
0
}
132
133
/*
134
 * Command validation context
135
 */
136
137
struct cmd_deleteheader_context_data {
138
  struct sieve_ast_argument *arg_index;
139
  struct sieve_ast_argument *arg_last;
140
};
141
142
/*
143
 * Tag validation
144
 */
145
146
static struct cmd_deleteheader_context_data *
147
cmd_deleteheader_get_context(struct sieve_command *cmd)
148
0
{
149
0
  struct cmd_deleteheader_context_data *ctx_data =
150
0
    (struct cmd_deleteheader_context_data *)cmd->data;
151
152
0
  if (ctx_data != NULL)
153
0
    return ctx_data;
154
155
0
  ctx_data = p_new(sieve_command_pool(cmd),
156
0
       struct cmd_deleteheader_context_data, 1);
157
0
  cmd->data = ctx_data;
158
159
0
  return ctx_data;
160
0
}
161
162
static bool
163
cmd_deleteheader_validate_index_tag(struct sieve_validator *valdtr,
164
            struct sieve_ast_argument **arg,
165
            struct sieve_command *cmd)
166
0
{
167
0
  struct sieve_ast_argument *tag = *arg;
168
0
  struct cmd_deleteheader_context_data *ctx_data;
169
0
  sieve_number_t index;
170
171
  /* Detach the tag itself */
172
0
  *arg = sieve_ast_arguments_detach(*arg,1);
173
174
  /* Check syntax:
175
   *   :index number
176
   */
177
0
  if (!sieve_validate_tag_parameter(valdtr, cmd, tag, *arg, NULL, 0,
178
0
            SAAT_NUMBER, FALSE))
179
0
    return FALSE;
180
181
0
  index = sieve_ast_argument_number(*arg);
182
0
  if (index > INT_MAX) {
183
0
    sieve_argument_validate_warning(
184
0
      valdtr, *arg,
185
0
      "the :%s tag for the %s %s has a parameter value '%llu' "
186
0
      "exceeding the maximum (%d)",
187
0
      sieve_argument_identifier(tag),
188
0
      sieve_command_identifier(cmd),
189
0
      sieve_command_type_name(cmd),
190
0
      (unsigned long long)index, INT_MAX);
191
0
    return FALSE;
192
0
  }
193
194
0
  ctx_data = cmd_deleteheader_get_context(cmd);
195
0
  ctx_data->arg_index = *arg;
196
197
  /* Skip parameter */
198
0
  *arg = sieve_ast_argument_next(*arg);
199
200
0
  return TRUE;
201
0
}
202
203
static bool
204
cmd_deleteheader_validate_last_tag(struct sieve_validator *valdtr ATTR_UNUSED,
205
           struct sieve_ast_argument **arg,
206
           struct sieve_command *cmd)
207
0
{
208
0
  struct cmd_deleteheader_context_data *ctx_data;
209
210
0
  ctx_data = cmd_deleteheader_get_context(cmd);
211
0
  ctx_data->arg_last = *arg;
212
213
  /* Skip parameter */
214
0
  *arg = sieve_ast_argument_next(*arg);
215
216
0
  return TRUE;
217
0
}
218
219
/*
220
 * Validation
221
 */
222
223
static bool
224
cmd_deleteheader_validate(struct sieve_validator *valdtr,
225
        struct sieve_command *cmd)
226
0
{
227
0
  struct sieve_ast_argument *arg = cmd->first_positional;
228
0
  struct cmd_deleteheader_context_data *ctx_data =
229
0
    (struct cmd_deleteheader_context_data *)cmd->data;
230
0
  struct sieve_comparator cmp_default =
231
0
    SIEVE_COMPARATOR_DEFAULT(i_ascii_casemap_comparator);
232
0
  struct sieve_match_type mcht_default =
233
0
    SIEVE_MATCH_TYPE_DEFAULT(is_match_type);
234
235
0
  if (ctx_data != NULL &&
236
0
      ctx_data->arg_last != NULL && ctx_data->arg_index == NULL) {
237
0
    sieve_argument_validate_error(
238
0
      valdtr, ctx_data->arg_last,
239
0
      "the :last tag for the %s %s cannot be specified "
240
0
      "without the :index tag",
241
0
      sieve_command_identifier(cmd),
242
0
      sieve_command_type_name(cmd));
243
0
    return FALSE;
244
0
  }
245
246
  /* Field name argument */
247
248
0
  if (arg == NULL) {
249
0
    sieve_command_validate_error(
250
0
      valdtr, cmd,
251
0
      "the %s %s expects at least one positional argument, "
252
0
      "but none was found",
253
0
      sieve_command_identifier(cmd),
254
0
      sieve_command_type_name(cmd));
255
0
    return FALSE;
256
0
  }
257
258
0
  if (!sieve_validate_positional_argument(valdtr, cmd, arg, "field name",
259
0
            1, SAAT_STRING_LIST))
260
0
    return FALSE;
261
0
  if (!sieve_validator_argument_activate(valdtr, cmd, arg, FALSE))
262
0
    return FALSE;
263
264
0
  if (sieve_argument_is_string_literal(arg)) {
265
0
    string_t *fname = sieve_ast_argument_str(arg);
266
267
0
    if (!rfc2822_header_field_name_verify(str_c(fname),
268
0
                  str_len(fname))) {
269
0
      sieve_argument_validate_error(
270
0
        valdtr, arg, "deleteheader command:"
271
0
        "specified field name '%s' is invalid",
272
0
        str_sanitize(str_c(fname), 80));
273
0
      return FALSE;
274
0
    }
275
276
0
    if (!ext_editheader_header_allow_delete(
277
0
      cmd->ext, str_c(fname))) {
278
0
      sieve_argument_validate_warning(
279
0
        valdtr, arg, "deleteheader command: "
280
0
        "deleting specified header field '%s' is forbidden; "
281
0
        "modification will be denied",
282
0
        str_sanitize(str_c(fname), 80));
283
0
    }
284
0
  }
285
286
  /* Value patterns argument */
287
288
0
  arg = sieve_ast_argument_next(arg);
289
0
  if (arg == NULL) {
290
    /* There is none; let's not generate code for useless match
291
       arguments */
292
0
    sieve_match_type_arguments_remove(valdtr, cmd);
293
0
    return TRUE;
294
0
  }
295
296
0
  if (!sieve_validate_positional_argument(
297
0
    valdtr, cmd, arg, "value patterns", 2, SAAT_STRING_LIST))
298
0
    return FALSE;
299
0
  if (!sieve_validator_argument_activate(valdtr, cmd, arg, FALSE))
300
0
    return FALSE;
301
302
  /* Validate the value patterns to a specified match type */
303
0
  return sieve_match_type_validate(valdtr, cmd, arg,
304
0
           &mcht_default, &cmp_default);
305
0
}
306
307
/*
308
 * Code generation
309
 */
310
311
static bool
312
cmd_deleteheader_generate(const struct sieve_codegen_env *cgenv,
313
        struct sieve_command *cmd)
314
0
{
315
0
  sieve_operation_emit(cgenv->sblock, cmd->ext, &deleteheader_operation);
316
317
  /* Generate arguments */
318
0
  if (!sieve_generate_arguments(cgenv, cmd, NULL))
319
0
    return FALSE;
320
321
  /* Emit a placeholder when the value-patterns argument is missing */
322
0
  if (sieve_ast_argument_next(cmd->first_positional) == NULL)
323
0
    sieve_opr_omitted_emit(cgenv->sblock);
324
325
0
  return TRUE;
326
0
}
327
328
/*
329
 * Code dump
330
 */
331
332
static bool
333
cmd_deleteheader_operation_dump(const struct sieve_dumptime_env *denv,
334
        sieve_size_t *address)
335
0
{
336
0
  int opt_code = 0;
337
338
0
  sieve_code_dumpf(denv, "DELETEHEADER");
339
0
  sieve_code_descend(denv);
340
341
  /* Optional operands */
342
0
  for (;;) {
343
0
    int opt;
344
345
0
    opt = sieve_match_opr_optional_dump(denv, address, &opt_code);
346
0
    if (opt < 0)
347
0
      return FALSE;
348
0
    if (opt == 0)
349
0
      break;
350
351
0
    switch (opt_code) {
352
0
    case OPT_INDEX:
353
0
      if (!sieve_opr_number_dump(denv, address, "index"))
354
0
        return FALSE;
355
0
      break;
356
0
    case OPT_LAST:
357
0
      sieve_code_dumpf(denv, "last");
358
0
      break;
359
0
    default:
360
0
      return FALSE;
361
0
    }
362
0
  };
363
364
0
  if (!sieve_opr_string_dump(denv, address, "field name"))
365
0
    return FALSE;
366
367
0
  return sieve_opr_stringlist_dump_ex(
368
0
    denv, address, "value patterns", "");
369
0
}
370
371
/*
372
 * Code execution
373
 */
374
375
static int
376
cmd_deleteheader_operation_execute(const struct sieve_runtime_env *renv,
377
           sieve_size_t *address)
378
0
{
379
0
  const struct sieve_extension *this_ext = renv->oprtn->ext;
380
0
  int opt_code = 0;
381
0
  struct sieve_comparator cmp =
382
0
    SIEVE_COMPARATOR_DEFAULT(i_ascii_casemap_comparator);
383
0
  struct sieve_match_type mcht =
384
0
    SIEVE_MATCH_TYPE_DEFAULT(is_match_type);
385
0
  string_t *field_name;
386
0
  struct sieve_stringlist *vpattern_list = NULL;
387
0
  struct edit_mail *edmail;
388
0
  sieve_number_t index_offset = 0;
389
0
  bool index_last = FALSE;
390
0
  bool trace = FALSE;
391
0
  int ret;
392
393
  /*
394
   * Read operands
395
   */
396
397
0
  for (;;) {
398
0
    int opt;
399
400
0
    opt = sieve_match_opr_optional_read(renv, address, &opt_code,
401
0
                &ret, &cmp, &mcht);
402
0
    if (opt < 0)
403
0
      return ret;
404
0
    if (opt == 0)
405
0
      break;
406
407
0
    switch (opt_code) {
408
0
    case OPT_INDEX:
409
0
      ret = sieve_opr_number_read(renv, address, "index",
410
0
                &index_offset);
411
0
      if (ret <= 0)
412
0
        return ret;
413
0
      if (index_offset > INT_MAX) {
414
0
        sieve_runtime_trace_error(
415
0
          renv, "index is > %d", INT_MAX);
416
0
        return SIEVE_EXEC_BIN_CORRUPT;
417
0
      }
418
0
      break;
419
0
    case OPT_LAST:
420
0
      index_last = TRUE;
421
0
      break;
422
0
    default:
423
0
      sieve_runtime_trace_error(
424
0
        renv, "unknown optional operand");
425
0
      return SIEVE_EXEC_BIN_CORRUPT;
426
0
    }
427
0
  }
428
429
  /* Read field-name */
430
0
  ret = sieve_opr_string_read(renv, address, "field-name", &field_name);
431
0
  if (ret <= 0)
432
0
    return ret;
433
434
  /* Read value-patterns */
435
0
  ret = sieve_opr_stringlist_read_ex(renv, address, "value-patterns",
436
0
             TRUE, &vpattern_list);
437
0
  if (ret <= 0)
438
0
    return ret;
439
440
  /*
441
   * Verify arguments
442
   */
443
444
0
  if (!rfc2822_header_field_name_verify(str_c(field_name),
445
0
                str_len(field_name))) {
446
0
    sieve_runtime_error(
447
0
      renv, NULL, "deleteheader action: "
448
0
      "specified field name '%s' is invalid",
449
0
      str_sanitize(str_c(field_name), 80));
450
0
    return SIEVE_EXEC_FAILURE;
451
0
  }
452
453
0
  if (!ext_editheader_header_allow_delete(this_ext, str_c(field_name))) {
454
0
    sieve_runtime_warning(
455
0
      renv, NULL, "deleteheader action: "
456
0
      "deleting specified header field '%s' is forbidden; "
457
0
      "modification denied",
458
0
      str_sanitize(str_c(field_name), 80));
459
0
    return SIEVE_EXEC_OK;
460
0
  }
461
462
  /*
463
   * Execute command
464
   */
465
466
0
  sieve_runtime_trace(renv, SIEVE_TRLVL_COMMANDS, "deleteheader command");
467
468
  /* Start editing the mail */
469
0
  edmail = sieve_message_edit(renv->msgctx);
470
471
0
  trace = sieve_runtime_trace_active(renv, SIEVE_TRLVL_COMMANDS);
472
473
  /* Either do string matching or just kill all/indexed notify action(s)
474
   */
475
0
  if (vpattern_list != NULL) {
476
0
    struct edit_mail_header_iter *edhiter;
477
0
    struct sieve_match_context *mctx;
478
479
0
    if (trace) {
480
0
      sieve_runtime_trace_descend(renv);
481
0
      if (index_offset != 0) {
482
0
        sieve_runtime_trace(
483
0
          renv, 0,
484
0
          "deleting matching occurrences of header '%s' at index %llu%s",
485
0
          str_c(field_name),
486
0
          (unsigned long long)index_offset,
487
0
          (index_last ? " from last": ""));
488
0
      } else {
489
0
        sieve_runtime_trace(
490
0
          renv, 0,
491
0
          "deleting matching occurrences of header '%s'",
492
0
          str_c(field_name));
493
0
      }
494
0
    }
495
496
    /* Iterate through all headers and delete those that match */
497
0
    ret = edit_mail_headers_iterate_init(edmail, str_c(field_name),
498
0
                 index_last, &edhiter);
499
0
    if (ret > 0) {
500
0
      int mret = 0;
501
0
      sieve_number_t pos = 0;
502
503
      /* Initialize match */
504
0
      mctx = sieve_match_begin(renv, &mcht, &cmp);
505
506
      /* Match */
507
0
      for (;;) {
508
0
        pos++;
509
510
        /* Check index if any */
511
0
        if (index_offset == 0 || pos == index_offset) {
512
0
          const char *value;
513
0
          int match;
514
515
          /* Match value against all value patterns */
516
0
          edit_mail_headers_iterate_get(edhiter, &value);
517
0
          match = sieve_match_value(
518
0
            mctx, value, strlen(value),
519
0
            vpattern_list);
520
0
          if (match < 0)
521
0
            break;
522
523
0
          if (match > 0) {
524
            /* Remove it and iterate to next */
525
0
            sieve_runtime_trace(
526
0
              renv, 0,
527
0
              "deleting header with value '%s'",
528
0
              value);
529
530
0
            if (!edit_mail_headers_iterate_remove(edhiter))
531
0
              break;
532
0
            continue;
533
0
          }
534
0
        }
535
536
0
        if (!edit_mail_headers_iterate_next(edhiter))
537
0
          break;
538
0
      }
539
540
      /* Finish match */
541
0
      mret = sieve_match_end(&mctx, &ret);
542
543
0
      edit_mail_headers_iterate_deinit(&edhiter);
544
0
      if (mret < 0)
545
0
        return ret;
546
0
    }
547
548
0
    if (ret == 0) {
549
0
      sieve_runtime_trace(renv, 0, "header '%s' not found",
550
0
              str_c(field_name));
551
0
    } else if (ret < 0) {
552
0
      sieve_runtime_warning(
553
0
        renv, NULL, "deleteheader action: "
554
0
        "failed to delete occurrences of header '%s' "
555
0
        "(this should not happen!)",
556
0
        str_c(field_name));
557
0
    }
558
559
0
  } else {
560
0
    int index = (index_last ?
561
0
           -((int)index_offset) : ((int)index_offset));
562
563
0
    if (trace) {
564
0
      sieve_runtime_trace_descend(renv);
565
0
      if (index_offset != 0) {
566
0
        sieve_runtime_trace(
567
0
          renv, 0,
568
0
          "deleting header '%s' at index %llu%s",
569
0
          str_c(field_name),
570
0
          (unsigned long long)index_offset,
571
0
          (index_last ? " from last": ""));
572
0
      } else {
573
0
        sieve_runtime_trace(
574
0
          renv, 0, "deleting header '%s'",
575
0
          str_c(field_name));
576
0
      }
577
0
    }
578
579
    /* Delete all occurrences of header */
580
0
    ret = edit_mail_header_delete(edmail, str_c(field_name), index);
581
0
    if (ret < 0) {
582
0
      sieve_runtime_warning(
583
0
        renv, NULL, "deleteheader action: "
584
0
        "failed to delete occurrences of header '%s' "
585
0
        "(this should not happen!)",
586
0
        str_c(field_name));
587
0
    } else if (trace) {
588
0
      sieve_runtime_trace(
589
0
        renv, 0,
590
0
        "deleted %d occurrences of header '%s'",
591
0
        ret, str_c(field_name));
592
0
    }
593
0
  }
594
595
0
  return SIEVE_EXEC_OK;
596
0
}