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/spamvirustest/ext-spamvirustest-common.c
Line
Count
Source
1
/* Copyright (c) Pigeonhole authors, see top-level COPYING file */
2
3
#include "lib.h"
4
#include "strfuncs.h"
5
#include "mail-storage.h"
6
#include "settings.h"
7
8
#include "sieve-common.h"
9
#include "sieve-error.h"
10
#include "sieve-extensions.h"
11
#include "sieve-message.h"
12
#include "sieve-interpreter.h"
13
#include "sieve-runtime-trace.h"
14
15
#include "ext-spamvirustest-settings.h"
16
#include "ext-spamvirustest-common.h"
17
18
#include "dregex.h"
19
20
#include <sys/types.h>
21
#include <ctype.h>
22
23
/*
24
 * Extension data
25
 */
26
27
struct ext_spamvirustest_header_spec {
28
  const char *header_name;
29
  struct dregex_code *regexp;
30
  bool regexp_match;
31
};
32
33
struct ext_spamvirustest_context {
34
  pool_t pool;
35
  unsigned int reload_id;
36
  const struct ext_spamvirustest_settings *set;
37
38
  struct ext_spamvirustest_header_spec status_header;
39
  struct ext_spamvirustest_header_spec score_max_header;
40
};
41
42
/*
43
 * Configuration parser
44
 */
45
46
static bool
47
ext_spamvirustest_header_spec_parse(
48
  pool_t pool, const char *data,
49
  struct ext_spamvirustest_header_spec *spec_r, const char **error_r)
50
0
{
51
0
  const char *p;
52
0
  const char *regexp_error;
53
54
0
  if (*data == '\0') {
55
0
    *error_r = "empty header specification";
56
0
    return FALSE;
57
0
  }
58
59
  /* Parse header name */
60
61
0
  p = data;
62
63
0
  while (*p == ' ' || *p == '\t')
64
0
    p++;
65
0
  while (*p != ':' && *p != '\0' && *p != ' ' && *p != '\t')
66
0
    p++;
67
68
0
  if (*p == '\0') {
69
0
    spec_r->header_name = p_strdup(pool, data);
70
0
    return TRUE;
71
0
  }
72
73
0
  spec_r->header_name = p_strdup_until(pool, data, p);
74
0
  while (*p == ' ' || *p == '\t')
75
0
    p++;
76
77
0
  if (*p == '\0') {
78
0
    spec_r->regexp_match = FALSE;
79
0
    return TRUE;
80
0
  }
81
82
  /* Parse and compile regular expression */
83
84
0
  if (*p != ':') {
85
0
    *error_r = t_strdup_printf("expecting ':', but found '%c'", *p);
86
0
    return FALSE;
87
0
  }
88
0
  p++;
89
0
  while (*p == ' ' || *p == '\t') p++;
90
91
0
  spec_r->regexp_match = TRUE;
92
0
  struct dregex_code *code = dregex_code_create();
93
94
0
  if (dregex_code_compile(code, p, DREGEX_ICASE|DREGEX_ASCII_ONLY, &regexp_error) < 0) {
95
0
    *error_r = t_strdup_printf(
96
0
      "failed to compile regular expression '%s': %s",
97
0
      p, regexp_error);
98
0
    dregex_code_free(&code);
99
0
    return FALSE;
100
0
  }
101
0
  spec_r->regexp = code;
102
0
  return TRUE;
103
0
}
104
105
static void
106
ext_spamvirustest_header_spec_free(struct ext_spamvirustest_header_spec *spec)
107
0
{
108
0
  dregex_code_free(&spec->regexp);
109
0
}
110
111
static bool
112
ext_spamvirustest_parse_strlen_value(const char *str_value,
113
             float *value_r, const char **error_r)
114
0
{
115
0
  const char *p = str_value;
116
0
  char ch = *p;
117
118
0
  if (*str_value == '\0') {
119
0
    *value_r = 0;
120
0
    return TRUE;
121
0
  }
122
123
0
  while (*p == ch)
124
0
    p++;
125
126
0
  if (*p != '\0') {
127
0
    *error_r = t_strdup_printf(
128
0
      "different character '%c' encountered in strlen value",
129
0
      *p);
130
0
    return FALSE;
131
0
  }
132
133
0
  *value_r = (p - str_value);
134
0
  return TRUE;
135
0
}
136
137
/*
138
 * Extension initialization
139
 */
140
141
int ext_spamvirustest_load(const struct sieve_extension *ext, void **context_r)
142
0
{
143
0
  static unsigned int reload_id = 0;
144
0
  struct sieve_instance *svinst = ext->svinst;
145
0
  struct ext_spamvirustest_context *extctx;
146
0
  const struct setting_parser_info *set_info;
147
0
  const struct ext_spamvirustest_settings *set;
148
0
  const char *error;
149
0
  pool_t pool;
150
0
  int ret = 0;
151
152
  /* Get settings */
153
154
0
  if (sieve_extension_is(ext, spamtest_extension) ||
155
0
      sieve_extension_is(ext, spamtestplus_extension))
156
0
    set_info = &ext_spamtest_setting_parser_info;
157
0
  else if (sieve_extension_is(ext, virustest_extension))
158
0
    set_info = &ext_virustest_setting_parser_info;
159
0
  else
160
0
    i_unreached();
161
162
0
  if (settings_get(svinst->event, set_info, 0, &set, &error) < 0) {
163
0
    e_error(svinst->event, "%s", error);
164
0
    return -1;
165
0
  }
166
167
  /* Base configuration */
168
169
0
  if (*set->status_header == '\0') {
170
0
    settings_free(set);
171
0
    return 0;
172
0
  }
173
174
0
  pool = pool_alloconly_create("spamvirustest_data", 512);
175
0
  extctx = p_new(pool, struct ext_spamvirustest_context, 1);
176
0
  extctx->pool = pool;
177
0
  extctx->reload_id = ++reload_id;
178
0
  extctx->set = set;
179
180
0
  if (!ext_spamvirustest_header_spec_parse(extctx->pool,
181
0
             set->status_header,
182
0
             &extctx->status_header,
183
0
             &error)) {
184
0
    e_error(svinst->event, "%s: "
185
0
      "Invalid status header specification '%s': %s",
186
0
      sieve_extension_name(ext), set->status_header, error);
187
0
    ret = -1;
188
0
  }
189
190
0
  if (ret == 0 &&
191
0
      set->parsed.status_type != EXT_SPAMVIRUSTEST_STATUS_TYPE_TEXT &&
192
0
      *set->score_max_header != '\0' &&
193
0
      !ext_spamvirustest_header_spec_parse(extctx->pool,
194
0
             set->score_max_header,
195
0
             &extctx->score_max_header,
196
0
             &error)) {
197
0
    e_error(svinst->event, "%s: "
198
0
      "Invalid max score header specification '%s': %s",
199
0
      sieve_extension_name(ext), set->score_max_header, error);
200
0
    ret = -1;
201
0
  }
202
203
0
  *context_r = extctx;
204
0
  if (ret < 0) {
205
0
    e_warning(svinst->event, "%s: "
206
0
        "Extension not configured, "
207
0
        "tests will always match against \"0\"",
208
0
        sieve_extension_name(ext));
209
0
    ext_spamvirustest_unload(ext);
210
0
    *context_r = NULL;
211
0
  }
212
213
0
  return ret;
214
0
}
215
216
void ext_spamvirustest_unload(const struct sieve_extension *ext)
217
0
{
218
0
  struct ext_spamvirustest_context *extctx = ext->context;
219
220
0
  if (extctx == NULL)
221
0
    return;
222
223
0
  ext_spamvirustest_header_spec_free(&extctx->status_header);
224
0
  ext_spamvirustest_header_spec_free(&extctx->score_max_header);
225
0
  settings_free(extctx->set);
226
0
  pool_unref(&extctx->pool);
227
0
}
228
229
/*
230
 * Runtime
231
 */
232
233
struct ext_spamvirustest_message_context {
234
  unsigned int reload_id;
235
  float score_ratio;
236
};
237
238
static const char *
239
ext_spamvirustest_get_score(const struct sieve_extension *ext,
240
          float score_ratio, bool percent)
241
0
{
242
0
  int score;
243
244
0
  if (score_ratio < 0)
245
0
    return "0";
246
247
0
  if (score_ratio > 1)
248
0
    score_ratio = 1;
249
250
0
  if (percent)
251
0
    score = score_ratio * 100 + 0.001;
252
0
  else if (sieve_extension_is(ext, virustest_extension))
253
0
    score = score_ratio * 4 + 1.001;
254
0
  else
255
0
    score = score_ratio * 9 + 1.001;
256
257
0
  return t_strdup_printf("%d", score);
258
0
}
259
260
int ext_spamvirustest_get_value(const struct sieve_runtime_env *renv,
261
        const struct sieve_extension *ext, bool percent,
262
        const char **value_r)
263
0
{
264
0
  struct ext_spamvirustest_context *extctx = ext->context;
265
0
  struct ext_spamvirustest_header_spec *status_header, *max_header;
266
0
  struct sieve_message_context *msgctx = renv->msgctx;
267
0
  struct ext_spamvirustest_message_context *mctx;
268
0
  struct mail *mail;
269
0
  ARRAY_TYPE(const_string) match_values;
270
0
  const char *header_value, *error;
271
0
  const char *status = NULL, *max = NULL;
272
0
  float status_value, max_value;
273
0
  unsigned int i, max_text;
274
0
  pool_t pool = sieve_interpreter_pool(renv->interp);
275
276
0
  *value_r = "0";
277
278
  /*
279
   * Check whether extension is properly configured
280
   */
281
282
0
  if (extctx == NULL) {
283
0
    sieve_runtime_trace(renv, SIEVE_TRLVL_TESTS,
284
0
            "error: extension not configured");
285
0
    return SIEVE_EXEC_OK;
286
0
  }
287
288
  /*
289
   * Check wether a cached result is available
290
   */
291
292
0
  mctx = (struct ext_spamvirustest_message_context *)
293
0
    sieve_message_context_extension_get(msgctx, ext);
294
295
0
  if (mctx == NULL) {
296
    /* Create new context */
297
0
    mctx = p_new(pool, struct ext_spamvirustest_message_context, 1);
298
0
    sieve_message_context_extension_set(msgctx, ext, mctx);
299
0
  } else if (mctx->reload_id == extctx->reload_id) {
300
    /* Use cached result */
301
0
    *value_r = ext_spamvirustest_get_score(ext, mctx->score_ratio,
302
0
                   percent);
303
0
    return SIEVE_EXEC_OK;
304
0
  } else {
305
    /* Extension was reloaded (probably in testsuite) */
306
0
  }
307
308
0
  mctx->reload_id = extctx->reload_id;
309
310
  /*
311
   * Get max status value
312
   */
313
314
0
  mail = sieve_message_get_mail(renv->msgctx);
315
0
  status_header = &extctx->status_header;
316
0
  max_header = &extctx->score_max_header;
317
318
0
  if (extctx->set->parsed.status_type != EXT_SPAMVIRUSTEST_STATUS_TYPE_TEXT) {
319
0
    if (max_header->header_name != NULL) {
320
      /* Get header from message */
321
0
      if (mail_get_first_header_utf8(
322
0
        mail, max_header->header_name,
323
0
        &header_value) < 0) {
324
0
        return sieve_runtime_mail_error  (
325
0
          renv, mail, "%s test: "
326
0
          "failed to read header field '%s'",
327
0
          sieve_extension_name(ext),
328
0
          max_header->header_name);
329
0
      }
330
0
      if (header_value == NULL) {
331
0
        sieve_runtime_trace(
332
0
          renv, SIEVE_TRLVL_TESTS,
333
0
          "header '%s' not found in message",
334
0
          max_header->header_name);
335
0
        goto failed;
336
0
      }
337
338
0
      if (max_header->regexp_match) {
339
0
        t_array_init(&match_values, 2);
340
0
        int ret;
341
        /* Execute regex */
342
0
        if ((ret = dregex_code_match_groups(max_header->regexp, header_value,
343
0
                   &match_values, &error)) < 1) {
344
0
          if (ret < 0) {
345
0
            sieve_runtime_trace(
346
0
              renv, SIEVE_TRLVL_TESTS,
347
0
              "score_max_header regexp for header '%s' failed"
348
0
              "on value '%s': %s",
349
0
              max_header->header_name,
350
0
              header_value,
351
0
              error);
352
0
          } else {
353
0
            sieve_runtime_trace(
354
0
              renv, SIEVE_TRLVL_TESTS,
355
0
              "score_max_header regexp for header '%s' did not match "
356
0
              "on value '%s'",
357
0
              max_header->header_name,
358
0
              header_value);
359
0
          }
360
0
          goto failed;
361
0
        }
362
0
        if (array_count(&match_values) < 2) {
363
0
          sieve_runtime_trace(
364
0
            renv, SIEVE_TRLVL_TESTS,
365
0
            "regexp did not return match value "
366
0
            "for string '%s'",
367
0
            header_value);
368
0
          goto failed;
369
0
        }
370
0
        max = array_idx_elem(&match_values, 1);
371
0
      } else {
372
0
        max = header_value;
373
0
      }
374
375
0
      if (!ext_spamvirustest_parse_decimal_value(
376
0
        max, &max_value, &error)) {
377
0
        sieve_runtime_trace(
378
0
          renv, SIEVE_TRLVL_TESTS,
379
0
          "failed to parse maximum value: %s",
380
0
          error);
381
0
        goto failed;
382
0
      }
383
0
    } else {
384
0
      max_value = extctx->set->parsed.score_max_value;
385
0
    }
386
387
0
    if (max_value == 0) {
388
0
      sieve_runtime_trace(renv, SIEVE_TRLVL_TESTS,
389
0
              "error: max value is 0");
390
0
      goto failed;
391
0
    }
392
0
  } else {
393
0
    max_value = (sieve_extension_is(ext, virustest_extension) ?
394
0
           5 : 10);
395
0
  }
396
397
  /*
398
   * Get status value
399
   */
400
401
  /* Get header from message */
402
0
  if (mail_get_first_header_utf8(mail, status_header->header_name,
403
0
               &header_value) < 0) {
404
0
    return sieve_runtime_mail_error(
405
0
      renv, mail, "%s test: failed to read header field '%s'",
406
0
      sieve_extension_name(ext), status_header->header_name);
407
0
  }
408
0
  if (header_value == NULL) {
409
0
    sieve_runtime_trace(renv, SIEVE_TRLVL_TESTS,
410
0
            "header '%s' not found in message",
411
0
            status_header->header_name);
412
0
    goto failed;
413
0
  }
414
415
  /* Execute regex */
416
0
  if (status_header->regexp_match) {
417
0
    int ret;
418
0
    t_array_init(&match_values, 2);
419
0
    if ((ret = dregex_code_match_groups(status_header->regexp, header_value,
420
0
               &match_values, &error)) < 1) {
421
0
      if (ret < 0) {
422
0
         sieve_runtime_trace(
423
0
          renv, SIEVE_TRLVL_TESTS,
424
0
          "status_header regexp for header '%s' did failed on value '%s': %s",
425
0
          status_header->header_name, header_value, error);
426
427
0
      } else {
428
0
        sieve_runtime_trace(
429
0
          renv, SIEVE_TRLVL_TESTS,
430
0
          "status_header regexp for header '%s' did not match on value '%s'",
431
0
          status_header->header_name, header_value);
432
0
      }
433
0
      goto failed;
434
0
    }
435
436
0
    if (array_count(&match_values) < 2) {
437
0
      sieve_runtime_trace(
438
0
        renv, SIEVE_TRLVL_TESTS,
439
0
        "regexp did not return match value for string '%s'",
440
0
        header_value);
441
0
      goto failed;
442
0
    }
443
0
    status = array_idx_elem(&match_values, 1);
444
0
  } else {
445
0
    status = header_value;
446
0
  }
447
448
0
  switch (extctx->set->parsed.status_type) {
449
0
  case EXT_SPAMVIRUSTEST_STATUS_TYPE_SCORE:
450
0
    if (!ext_spamvirustest_parse_decimal_value(
451
0
      status, &status_value, &error)) {
452
0
      sieve_runtime_trace(
453
0
        renv, SIEVE_TRLVL_TESTS,
454
0
        "failed to parse status value '%s': %s",
455
0
        status, error);
456
0
      goto failed;
457
0
    }
458
0
    break;
459
0
  case EXT_SPAMVIRUSTEST_STATUS_TYPE_STRLEN:
460
0
    if (!ext_spamvirustest_parse_strlen_value(
461
0
      status, &status_value, &error)) {
462
0
      sieve_runtime_trace(
463
0
        renv, SIEVE_TRLVL_TESTS,
464
0
        "failed to parse status value '%s': %s",
465
0
        status, error);
466
0
      goto failed;
467
0
    }
468
0
    break;
469
0
  case EXT_SPAMVIRUSTEST_STATUS_TYPE_TEXT:
470
0
    max_text = (sieve_extension_is(ext, virustest_extension) ?
471
0
          5 : 10);
472
0
    status_value = 0;
473
474
0
    i = 0;
475
0
    while (i <= max_text) {
476
0
      if (extctx->set->parsed.text_values[i] != NULL &&
477
0
          strcmp(status,
478
0
           extctx->set->parsed.text_values[i]) == 0) {
479
0
        status_value = (float)i;
480
0
        break;
481
0
      }
482
0
      i++;
483
0
    }
484
485
0
    if (i > max_text) {
486
0
      sieve_runtime_trace(
487
0
        renv, SIEVE_TRLVL_TESTS,
488
0
        "failed to match textstatus value '%s'",
489
0
        status);
490
0
      goto failed;
491
0
    }
492
0
    break;
493
0
  default:
494
0
    i_unreached();
495
0
  }
496
497
  /* Calculate value */
498
0
  if (status_value < 0)
499
0
    mctx->score_ratio = 0;
500
0
  else if (status_value > max_value)
501
0
    mctx->score_ratio = 1;
502
0
  else
503
0
    mctx->score_ratio = (status_value / max_value);
504
505
0
  sieve_runtime_trace(
506
0
    renv, SIEVE_TRLVL_TESTS,
507
0
    "extracted score=%.3f, max=%.3f, ratio=%.0f %%",
508
0
    status_value, max_value, mctx->score_ratio * 100);
509
510
0
  *value_r = ext_spamvirustest_get_score(ext, mctx->score_ratio, percent);
511
0
  return SIEVE_EXEC_OK;
512
513
0
failed:
514
0
  mctx->score_ratio = -1;
515
0
  *value_r = "0";
516
0
  return SIEVE_EXEC_OK;
517
0
}