diff --git a/src/lib-sieve/plugins/spamvirustest/Makefile.am b/src/lib-sieve/plugins/spamvirustest/Makefile.am index 6a15bdd148d53cceea365ed8c6b58c6d2d2d5cc6..ebb7143ffbd72116c22e26da7342deee21d199ad 100644 --- a/src/lib-sieve/plugins/spamvirustest/Makefile.am +++ b/src/lib-sieve/plugins/spamvirustest/Makefile.am @@ -9,8 +9,10 @@ tests = \ libsieve_ext_spamvirustest_la_SOURCES = \ $(tests) \ + ext-spamvirustest-settings.c \ ext-spamvirustest-common.c \ ext-spamvirustest.c noinst_HEADERS = \ + ext-spamvirustest-settings.h \ ext-spamvirustest-common.h diff --git a/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-common.c b/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-common.c index a8a47daa5ef41b057d07df4a2fa4582276f4bc32..c1a4b3a6e92570854ff3ee9b1d2f4cb2c4cd0c5a 100644 --- a/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-common.c +++ b/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-common.c @@ -4,31 +4,26 @@ #include "lib.h" #include "strfuncs.h" #include "mail-storage.h" +#include "settings.h" #include "sieve-common.h" -#include "sieve-settings.old.h" #include "sieve-error.h" #include "sieve-extensions.h" #include "sieve-message.h" #include "sieve-interpreter.h" #include "sieve-runtime-trace.h" +#include "ext-spamvirustest-settings.h" #include "ext-spamvirustest-common.h" #include <sys/types.h> -#include <regex.h> #include <ctype.h> +#include <regex.h> /* * Extension data */ -enum ext_spamvirustest_status_type { - EXT_SPAMVIRUSTEST_STATUS_TYPE_SCORE, - EXT_SPAMVIRUSTEST_STATUS_TYPE_STRLEN, - EXT_SPAMVIRUSTEST_STATUS_TYPE_TEXT, -}; - struct ext_spamvirustest_header_spec { const char *header_name; regex_t regexp; @@ -38,15 +33,10 @@ struct ext_spamvirustest_header_spec { struct ext_spamvirustest_context { pool_t pool; unsigned int reload_id; + const struct ext_spamvirustest_settings *set; struct ext_spamvirustest_header_spec status_header; struct ext_spamvirustest_header_spec max_header; - - enum ext_spamvirustest_status_type status_type; - - float max_value; - - const char *text_values[11]; }; /* @@ -96,9 +86,9 @@ _regexp_match_get_value(const char *string, int index, regmatch_t pmatch[], */ static bool -ext_spamvirustest_header_spec_parse(struct ext_spamvirustest_header_spec *spec, - pool_t pool, const char *data, - const char **error_r) +ext_spamvirustest_header_spec_parse( + pool_t pool, const char *data, + struct ext_spamvirustest_header_spec *spec_r, const char **error_r) { const char *p; const char *regexp_error; @@ -118,16 +108,16 @@ ext_spamvirustest_header_spec_parse(struct ext_spamvirustest_header_spec *spec, p++; if (*p == '\0') { - spec->header_name = p_strdup(pool, data); + spec_r->header_name = p_strdup(pool, data); return TRUE; } - spec->header_name = p_strdup_until(pool, data, p); + spec_r->header_name = p_strdup_until(pool, data, p); while (*p == ' ' || *p == '\t') p++; if (*p == '\0') { - spec->regexp_match = FALSE; + spec_r->regexp_match = FALSE; return TRUE; } @@ -140,8 +130,8 @@ ext_spamvirustest_header_spec_parse(struct ext_spamvirustest_header_spec *spec, p++; while (*p == ' ' || *p == '\t') p++; - spec->regexp_match = TRUE; - if (!_regexp_compile(&spec->regexp, p, ®exp_error)) { + spec_r->regexp_match = TRUE; + if (!_regexp_compile(&spec_r->regexp, p, ®exp_error)) { *error_r = t_strdup_printf( "failed to compile regular expression '%s': %s", p, regexp_error); @@ -182,68 +172,6 @@ ext_spamvirustest_parse_strlen_value(const char *str_value, return TRUE; } -static bool -ext_spamvirustest_parse_decimal_value(const char *str_value, - float *value_r, const char **error_r) -{ - const char *p = str_value; - float value; - float sign = 1; - int digits; - - if (*p == '\0') { - *error_r = "empty value"; - return FALSE; - } - - if (*p == '+' || *p == '-') { - if (*p == '-') - sign = -1; - p++; - } - - value = 0; - digits = 0; - while (i_isdigit(*p)) { - value = value*10 + (*p-'0'); - if (digits++ > 4) { - *error_r = t_strdup_printf( - "decimal value has too many digits before radix point: %s", - str_value); - return FALSE; - } - p++; - } - - if (*p == '.' || *p == ',') { - float radix = .1; - p++; - - digits = 0; - while (i_isdigit(*p)) { - value = value + (*p-'0')*radix; - - if (digits++ > 4) { - *error_r = t_strdup_printf( - "decimal value has too many digits after radix point: %s", - str_value); - return FALSE; - } - radix /= 10; - p++; - } - } - - if (*p != '\0') { - *error_r = t_strdup_printf( - "invalid decimal point value: %s", str_value); - return FALSE; - } - - *value_r = value * sign; - return TRUE; -} - /* * Extension initialization */ @@ -253,158 +181,69 @@ int ext_spamvirustest_load(const struct sieve_extension *ext, void **context_r) static unsigned int reload_id = 0; struct sieve_instance *svinst = ext->svinst; struct ext_spamvirustest_context *extctx; - const char *ext_name, *status_header, *max_header, *status_type, - *max_value; - enum ext_spamvirustest_status_type type; + const struct setting_parser_info *set_info; + const struct ext_spamvirustest_settings *set; const char *error; pool_t pool; int ret = 0; - /* FIXME: Prevent loading of both spamtest and spamtestplus: - let these share contexts. - */ - - if (sieve_extension_is(ext, spamtest_extension) || - sieve_extension_is(ext, spamtestplus_extension)) { - ext_name = spamtest_extension.name; - } else { - ext_name = sieve_extension_name(ext); - } - /* Get settings */ - status_header = sieve_setting_get( - svinst, t_strconcat("sieve_", ext_name, - "_status_header", NULL)); - status_type = sieve_setting_get( - svinst, t_strconcat("sieve_", ext_name, "_status_type", NULL)); - max_header = sieve_setting_get( - svinst, t_strconcat("sieve_", ext_name, "_max_header", NULL)); - max_value = sieve_setting_get( - svinst, t_strconcat("sieve_", ext_name, "_max_value", NULL)); - - /* Base configuration */ - - if (status_header == NULL) - return 0; + if (sieve_extension_is(ext, spamtest_extension) || + sieve_extension_is(ext, spamtestplus_extension)) + set_info = &ext_spamtest_setting_parser_info; + else if (sieve_extension_is(ext, virustest_extension)) + set_info = &ext_virustest_setting_parser_info; + else + i_unreached(); - if (status_type == NULL || strcmp(status_type, "score") == 0) { - type = EXT_SPAMVIRUSTEST_STATUS_TYPE_SCORE; - } else if (strcmp(status_type, "strlen") == 0) { - type = EXT_SPAMVIRUSTEST_STATUS_TYPE_STRLEN; - } else if (strcmp(status_type, "text") == 0) { - type = EXT_SPAMVIRUSTEST_STATUS_TYPE_TEXT; - } else { - e_error(svinst->event, "%s: " - "invalid status type '%s'", ext_name, status_type); + if (settings_get(svinst->event, set_info, 0, &set, &error) < 0) { + e_error(svinst->event, "%s", error); return -1; } - /* Verify settings */ - - if (type != EXT_SPAMVIRUSTEST_STATUS_TYPE_TEXT) { - - if (max_header != NULL && max_value != NULL) { - e_error(svinst->event, "%s: " - "sieve_%s_max_header and sieve_%s_max_value " - "cannot both be configured", - ext_name, ext_name, ext_name); - return 0; - } - - if (max_header == NULL && max_value == NULL) { - e_error(svinst->event, "%s: " - "none of sieve_%s_max_header or sieve_%s_max_value " - "is configured", ext_name, ext_name, ext_name); - return 0; - } - } else { - if (max_header != NULL) { - e_warning(svinst->event, "%s: " - "setting sieve_%s_max_header has no meaning " - "for sieve_%s_status_type=text", - ext_name, ext_name, ext_name); - } + /* Base configuration */ - if (max_value != NULL) { - e_warning(svinst->event, "%s: " - "setting sieve_%s_max_value has no meaning " - "for sieve_%s_status_type=text", - ext_name, ext_name, ext_name); - } + if (*set->status_header == '\0') { + settings_free(set); + return 0; } pool = pool_alloconly_create("spamvirustest_data", 512); extctx = p_new(pool, struct ext_spamvirustest_context, 1); extctx->pool = pool; extctx->reload_id = ++reload_id; - extctx->status_type = type; + extctx->set = set; - if (!ext_spamvirustest_header_spec_parse( - &extctx->status_header, extctx->pool, status_header, - &error)) { + if (!ext_spamvirustest_header_spec_parse(extctx->pool, + set->status_header, + &extctx->status_header, + &error)) { e_error(svinst->event, "%s: " - "invalid status header specification '%s': %s", - ext_name, status_header, error); + "Invalid status header specification '%s': %s", + sieve_extension_name(ext), set->status_header, error); ret = -1; } - if (ret == 0) { - if (type != EXT_SPAMVIRUSTEST_STATUS_TYPE_TEXT) { - /* Parse max header */ - - if (max_header != NULL && - !ext_spamvirustest_header_spec_parse( - &extctx->max_header, extctx->pool, - max_header, &error)) { - e_error(svinst->event, "%s: " - "invalid max header specification " - "'%s': %s", ext_name, max_header, - error); - ret = -1; - } - - /* Parse max value */ - - if (ret == 0 && max_value != NULL) { - if (!ext_spamvirustest_parse_decimal_value( - max_value, &extctx->max_value, &error)) { - e_error(svinst->event, "%s: " - "invalid max value specification " - "'%s': %s", ext_name, max_value, - error); - ret = -1; - } - } - - } else { - unsigned int i, max_text; - - max_text = (sieve_extension_is(ext, virustest_extension) ? - 5 : 10); - - /* Get text values */ - for (i = 0; i <= max_text; i++) { - const char *value = sieve_setting_get( - svinst, t_strdup_printf("sieve_%s_text_value%d", - ext_name, i)); - - if (value != NULL && *value != '\0') { - extctx->text_values[i] = - p_strdup(extctx->pool, value); - } - } - - extctx->max_value = 1; - } + if (ret == 0 && + set->parsed.status_type != EXT_SPAMVIRUSTEST_STATUS_TYPE_TEXT && + *set->max_header != '\0' && + !ext_spamvirustest_header_spec_parse(extctx->pool, + set->max_header, + &extctx->max_header, + &error)) { + e_error(svinst->event, "%s: " + "Invalid max header specification '%s': %s", + sieve_extension_name(ext), set->max_header, error); + ret = -1; } *context_r = extctx; if (ret < 0) { e_warning(svinst->event, "%s: " - "extension not configured, " + "Extension not configured, " "tests will always match against \"0\"", - ext_name); + sieve_extension_name(ext)); ext_spamvirustest_unload(ext); *context_r = NULL; } @@ -421,6 +260,7 @@ void ext_spamvirustest_unload(const struct sieve_extension *ext) ext_spamvirustest_header_spec_free(&extctx->status_header); ext_spamvirustest_header_spec_free(&extctx->max_header); + settings_free(extctx->set); pool_unref(&extctx->pool); } @@ -513,7 +353,7 @@ int ext_spamvirustest_get_value(const struct sieve_runtime_env *renv, status_header = &extctx->status_header; max_header = &extctx->max_header; - if (extctx->status_type != EXT_SPAMVIRUSTEST_STATUS_TYPE_TEXT) { + if (extctx->set->parsed.status_type != EXT_SPAMVIRUSTEST_STATUS_TYPE_TEXT) { if (max_header->header_name != NULL) { /* Get header from message */ if (mail_get_first_header_utf8( @@ -569,7 +409,7 @@ int ext_spamvirustest_get_value(const struct sieve_runtime_env *renv, goto failed; } } else { - max_value = extctx->max_value; + max_value = extctx->set->parsed.max_value; } if (max_value == 0) { @@ -624,7 +464,7 @@ int ext_spamvirustest_get_value(const struct sieve_runtime_env *renv, status = header_value; } - switch (extctx->status_type) { + switch (extctx->set->parsed.status_type) { case EXT_SPAMVIRUSTEST_STATUS_TYPE_SCORE: if (!ext_spamvirustest_parse_decimal_value( status, &status_value, &error)) { @@ -652,8 +492,9 @@ int ext_spamvirustest_get_value(const struct sieve_runtime_env *renv, i = 0; while (i <= max_text) { - if (extctx->text_values[i] != NULL && - strcmp(status, extctx->text_values[i]) == 0) { + if (extctx->set->parsed.text_values[i] != NULL && + strcmp(status, + extctx->set->parsed.text_values[i]) == 0) { status_value = (float)i; break; } diff --git a/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-settings.c b/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-settings.c new file mode 100644 index 0000000000000000000000000000000000000000..eb8d2f1413b1437323411e8c46732335c9035089 --- /dev/null +++ b/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-settings.c @@ -0,0 +1,244 @@ +/* Copyright (c) 2024 Pigeonhole authors, see the included COPYING file + */ + +#include "lib.h" +#include "array.h" +#include "settings.h" +#include "settings-parser.h" + +#include "ext-spamvirustest-settings.h" + +#include <ctype.h> + +static bool +ext_spamtest_settings_check(void *_set, pool_t pool, const char **error_r); +static bool +ext_virustest_settings_check(void *_set, pool_t pool, const char **error_r); + +#undef DEF +#define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type("sieve_spamtest_"#name, name, \ + struct ext_spamvirustest_settings) + +static const struct setting_define ext_spamtest_setting_defines[] = { + DEF(STR, status_header), + DEF(STR, status_type), + + DEF(STR, max_header), + DEF(STR, max_value), + + { .type = SET_STRLIST, .key = "sieve_spamtest_text_value", + .offset = offsetof(struct ext_spamvirustest_settings, + text_value) }, + + SETTING_DEFINE_LIST_END, +}; + +#undef DEF +#define DEF(type, name) \ + SETTING_DEFINE_STRUCT_##type("sieve_virustest_"#name, name, \ + struct ext_spamvirustest_settings) + +static const struct setting_define ext_virustest_setting_defines[] = { + DEF(STR, status_header), + DEF(STR, status_type), + + DEF(STR, max_header), + DEF(STR, max_value), + + { .type = SET_STRLIST, .key = "sieve_virustest_text_value", + .offset = offsetof(struct ext_spamvirustest_settings, + text_value) }, + + SETTING_DEFINE_LIST_END, +}; + +static const struct ext_spamvirustest_settings ext_spamvirustest_default_settings = { + .status_header = "", + .status_type = "", + + .max_header = "", + .max_value = "", + + .text_value = ARRAY_INIT, +}; + +const struct setting_parser_info ext_spamtest_setting_parser_info = { + .name = "sieve_spamtest", + + .defines = ext_spamtest_setting_defines, + .defaults = &ext_spamvirustest_default_settings, + + .struct_size = sizeof(struct ext_spamvirustest_settings), + + .check_func = ext_spamtest_settings_check, + + .pool_offset1 = 1 + offsetof(struct ext_spamvirustest_settings, pool), +}; + +const struct setting_parser_info ext_virustest_setting_parser_info = { + .name = "sieve_virustest", + + .defines = ext_virustest_setting_defines, + .defaults = &ext_spamvirustest_default_settings, + + .struct_size = sizeof(struct ext_spamvirustest_settings), + + .check_func = ext_virustest_settings_check, + + .pool_offset1 = 1 + offsetof(struct ext_spamvirustest_settings, pool), +}; + +/* <settings checks> */ +bool ext_spamvirustest_parse_decimal_value(const char *str_value, + float *value_r, const char **error_r) +{ + const char *p = str_value; + float value; + float sign = 1; + int digits; + + if (*p == '\0') { + *error_r = "empty value"; + return FALSE; + } + + if (*p == '+' || *p == '-') { + if (*p == '-') + sign = -1; + p++; + } + + value = 0; + digits = 0; + while (i_isdigit(*p)) { + value = value*10 + (*p-'0'); + if (digits++ > 4) { + *error_r = t_strdup_printf( + "Decimal value has too many digits before radix point: %s", + str_value); + return FALSE; + } + p++; + } + + if (*p == '.' || *p == ',') { + float radix = .1; + p++; + + digits = 0; + while (i_isdigit(*p)) { + value = value + (*p-'0')*radix; + + if (digits++ > 4) { + *error_r = t_strdup_printf( + "Decimal value has too many digits after radix point: %s", + str_value); + return FALSE; + } + radix /= 10; + p++; + } + } + + if (*p != '\0') { + *error_r = t_strdup_printf( + "Invalid decimal point value: %s", str_value); + return FALSE; + } + + *value_r = value * sign; + return TRUE; +} + +static bool +ext_spamvirustest_settings_check(void *_set, bool virustest, + pool_t pool ATTR_UNUSED, const char **error_r) +{ + struct ext_spamvirustest_settings *set = _set; + const char *ext_name = (virustest ? "virustest" : "spamtest"); + const char *error; + + if (*set->status_header == '\0') + return TRUE; + + if (*set->status_type == '\0' || + strcmp(set->status_type, "score") == 0) + set->parsed.status_type = EXT_SPAMVIRUSTEST_STATUS_TYPE_SCORE; + else if (strcmp(set->status_type, "strlen") == 0) + set->parsed.status_type = EXT_SPAMVIRUSTEST_STATUS_TYPE_STRLEN; + else if (strcmp(set->status_type, "text") == 0) + set->parsed.status_type = EXT_SPAMVIRUSTEST_STATUS_TYPE_TEXT; + else { + *error_r = t_strdup_printf("Invalid status type '%s'", + set->status_type); + return FALSE; + } + + if (set->parsed.status_type != EXT_SPAMVIRUSTEST_STATUS_TYPE_TEXT) { + if (*set->max_header != '\0' && *set->max_value != '\0') { + *error_r = t_strdup_printf( + "sieve_%s_max_header and sieve_%s_max_value " + "cannot both be configured", + ext_name, ext_name); + return FALSE; + } + if (*set->max_header == '\0' && *set->max_value == '\0') { + *error_r = t_strdup_printf( + "None of sieve_%s_max_header or sieve_%s_max_value " + "is configured", ext_name, ext_name); + return FALSE; + } + if (*set->max_value != '\0' && + !ext_spamvirustest_parse_decimal_value( + set->max_value, &set->parsed.max_value, &error)) { + *error_r = t_strdup_printf( + "Invalid max value specification " + "'%s': %s", set->max_value, error); + return FALSE; + } + } else { + const char *const *tvalues; + unsigned int tvalues_count, i; + unsigned int tv_index_max = (virustest ? 5 : 10); + + tvalues = array_get(&set->text_value, &tvalues_count); + i_assert(tvalues_count % 2 == 0); + for (i = 0; i < tvalues_count; i += 2) { + unsigned int tv_index; + + if (str_to_uint(tvalues[i], &tv_index) < 0) { + *error_r = t_strdup_printf( + "Invalid text value index '%s'", + tvalues[i]); + return FALSE; + } + if (tv_index > tv_index_max) { + *error_r = t_strdup_printf( + "Text value index out of range " + "(%u > %u)", tv_index, tv_index_max); + return FALSE; + } + set->parsed.text_values[tv_index] = tvalues[i + 1]; + } + set->parsed.max_value = 1; + } + + return TRUE; +} + +static bool +ext_spamtest_settings_check(void *_set, pool_t pool ATTR_UNUSED, + const char **error_r) +{ + return ext_spamvirustest_settings_check(_set, FALSE, pool, error_r); +} + +static bool +ext_virustest_settings_check(void *_set, pool_t pool ATTR_UNUSED, + const char **error_r) +{ + return ext_spamvirustest_settings_check(_set, TRUE, pool, error_r); +} +/* </settings checks> */ + diff --git a/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-settings.h b/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-settings.h new file mode 100644 index 0000000000000000000000000000000000000000..ce1ba5f2d117207f478a1e8388e432a5b12e939f --- /dev/null +++ b/src/lib-sieve/plugins/spamvirustest/ext-spamvirustest-settings.h @@ -0,0 +1,40 @@ +#ifndef EXT_SPAMVIRUSTEST_SETTINGS_H +#define EXT_SPAMVIRUSTEST_SETTINGS_H + +/* <settings checks> */ +enum ext_spamvirustest_status_type { + EXT_SPAMVIRUSTEST_STATUS_TYPE_SCORE, + EXT_SPAMVIRUSTEST_STATUS_TYPE_STRLEN, + EXT_SPAMVIRUSTEST_STATUS_TYPE_TEXT, +}; +/* </settings checks> */ + +struct ext_spamvirustest_settings { + pool_t pool; + + const char *status_header; + const char *status_type; + const char *max_header; + const char *max_value; + + ARRAY_TYPE(const_string) text_value; + + struct { + enum ext_spamvirustest_status_type status_type; + + float max_value; + + const char *text_values[11]; + } parsed; +}; + +extern const struct setting_parser_info ext_spamtest_setting_parser_info; +extern const struct setting_parser_info ext_virustest_setting_parser_info; + +/* <settings checks> */ +bool ext_spamvirustest_parse_decimal_value(const char *str_value, + float *value_r, + const char **error_r); +/* </settings checks> */ + +#endif diff --git a/tests/extensions/spamvirustest/spamtest.svtest b/tests/extensions/spamvirustest/spamtest.svtest index 11ffdeefd2cabee720b79d87dce215ae46daff2f..9db1d4cf9e58079b76d4a6b3e08a861d787045f3 100644 --- a/tests/extensions/spamvirustest/spamtest.svtest +++ b/tests/extensions/spamvirustest/spamtest.svtest @@ -243,8 +243,8 @@ Test! test_config_set "sieve_spamtest_status_header" "X-Spam-Verdict"; test_config_set "sieve_spamtest_status_type" "text"; -test_config_set "sieve_spamtest_text_value1" "Not Spam"; -test_config_set "sieve_spamtest_text_value10" "Spam"; +test_config_set "sieve_spamtest_text_value/1" "Not Spam"; +test_config_set "sieve_spamtest_text_value/10" "Spam"; test_config_unset "sieve_spamtest_max_header"; test_config_unset "sieve_spamtest_max_value"; test_config_reload :extension "spamtest"; diff --git a/tests/extensions/spamvirustest/virustest.svtest b/tests/extensions/spamvirustest/virustest.svtest index 03bb141c75df725713023c4aa80d791a38a3b1a3..1a6c1d42f1f6511a9a525b1f1b8ea9dd8d885ff9 100644 --- a/tests/extensions/spamvirustest/virustest.svtest +++ b/tests/extensions/spamvirustest/virustest.svtest @@ -27,11 +27,11 @@ Test! test_config_set "sieve_virustest_status_header" "X-VirusCheck"; test_config_set "sieve_virustest_status_type" "text"; -test_config_set "sieve_virustest_text_value1" "Clean"; -test_config_set "sieve_virustest_text_value2" "Presumed Clean"; -test_config_set "sieve_virustest_text_value3" "Not sure"; -test_config_set "sieve_virustest_text_value4" "Almost Certain"; -test_config_set "sieve_virustest_text_value5" "Definitely"; +test_config_set "sieve_virustest_text_value/1" "Clean"; +test_config_set "sieve_virustest_text_value/2" "Presumed Clean"; +test_config_set "sieve_virustest_text_value/3" "Not sure"; +test_config_set "sieve_virustest_text_value/4" "Almost Certain"; +test_config_set "sieve_virustest_text_value/5" "Definitely"; test_config_reload :extension "virustest"; test "Text: 5" { @@ -103,8 +103,8 @@ test "Text: 1" { test_config_set "sieve_virustest_status_header" "X-Virus-Scan:Found to be (.+)\."; test_config_set "sieve_virustest_status_type" "text"; -test_config_set "sieve_virustest_text_value1" "clean"; -test_config_set "sieve_virustest_text_value5" "infected"; +test_config_set "sieve_virustest_text_value/1" "clean"; +test_config_set "sieve_virustest_text_value/5" "infected"; test_config_reload :extension "virustest"; test "Text: regex: 1" {