end
local function group_transform(cfg, k, v)
+ if v:type() ~= 'object' then
+ logger.warnx(rspamd_config, 'invalid group definition for %s: expected object', k)
+ return
+ end
+
if v:at('name') then
k = v:at('name'):unwrap()
end
if v:at('disabled') then
new_group.disabled = v:at('disabled'):unwrap()
end
- if v.max_score then
+ if v:at('max_score') then
new_group.max_score = v:at('max_score'):unwrap()
end
- if v:at('symbol') then
- for sk, sv in v:at('symbol'):pairs() do
- if sv:at('name') then
+ local symbol_section = v:at('symbol')
+ if symbol_section and symbol_section:type() == 'object' then
+ for sk, sv in symbol_section:pairs() do
+ if sv:type() == 'object' and sv:at('name') then
sk = sv:at('name'):unwrap()
sv.name = nil -- Remove field
end
cfg.group = {}
end
- if cfg:at('group'):at(k) then
- cfg:at('group')[k] = lua_util.override_defaults(cfg:at('group')[k]:unwrap(), new_group)
+ local cfg_group = cfg:at('group')
+ if cfg_group:at(k) and cfg_group:at(k):type() == 'object' then
+ cfg_group[k] = lua_util.override_defaults(cfg_group:at(k):unwrap(), new_group)
else
- cfg:at('group')[k] = new_group
+ cfg_group[k] = new_group
end
logger.infox("overriding group %s from the legacy metric settings", k)
local function symbol_transform(cfg, k, v)
local groups = cfg:at('group')
- if groups then
+ if groups and groups:type() == 'object' then
-- first try to find any group where there is a definition of this symbol
for gr_n, gr in groups:pairs() do
- local symbols = gr:at('symbols')
- if symbols and symbols:at(k) then
- -- We override group symbol with ungrouped symbol
- logger.infox("overriding group symbol %s in the group %s", k, gr_n)
- symbols[k] = lua_util.override_defaults(symbols:at(k):unwrap(), v:unwrap())
- return
+ if gr:type() == 'object' then
+ local symbols = gr:at('symbols')
+ if symbols and symbols:type() == 'object' and symbols:at(k) then
+ -- We override group symbol with ungrouped symbol
+ logger.infox("overriding group symbol %s in the group %s", k, gr_n)
+ symbols[k] = lua_util.override_defaults(symbols:at(k):unwrap(), v:unwrap())
+ return
+ end
end
end
end
-- Now check what Rspamd knows about this symbol
local sym = rspamd_config:get_symbol(k)
- if groups and (not sym or not sym.group) then
+ if groups and groups:type() == 'object' and (not sym or not sym.group) then
-- Otherwise we just use group 'ungrouped'
- if not groups:at('ungrouped') then
+ local ungrouped = groups:at('ungrouped')
+ if not ungrouped or ungrouped:type() ~= 'object' then
groups.ungrouped = {
symbols = {
[k] = v
}
}
else
- groups:at('ungrouped'):at('symbols')[k] = v
+ local ug_symbols = ungrouped:at('symbols')
+ if not ug_symbols or ug_symbols:type() ~= 'object' then
+ ungrouped.symbols = { [k] = v }
+ else
+ ug_symbols[k] = v
+ end
end
logger.debugx("adding symbol %s to the group 'ungrouped'", k)
end
end
+-- Helper to run a transform function with error handling
+local function try_transform(name, func)
+ local ok, err = pcall(func)
+ if not ok then
+ logger.errx(rspamd_config, 'config transform %s failed: %s', name, err)
+ return false
+ end
+ return true
+end
+
return function(cfg)
local ret = false
if cfg:at('metric') then
- local metric = cfg:at('metric')
-
- -- There are two things that we can have (old `metric_pairs` logic)
- -- 1. A metric is a single metric definition like: metric { name = "default", ... }
- -- 2. A metric is a list of metrics like: metric { "default": ... }
- if metric:at('actions') or metric:at('name') then
- convert_metric(cfg, metric)
- else
- for _, v in cfg:at('metric'):pairs() do
- if v:type() == 'object' then
- logger.infox('converting metric element %s', v)
- convert_metric(cfg, v)
+ try_transform('metric', function()
+ local metric = cfg:at('metric')
+
+ -- There are two things that we can have (old `metric_pairs` logic)
+ -- 1. A metric is a single metric definition like: metric { name = "default", ... }
+ -- 2. A metric is a list of metrics like: metric { "default": ... }
+ if metric:at('actions') or metric:at('name') then
+ convert_metric(cfg, metric)
+ else
+ for _, v in cfg:at('metric'):pairs() do
+ if v:type() == 'object' then
+ logger.infox('converting metric element %s', v)
+ convert_metric(cfg, v)
+ end
end
end
- end
- ret = true
+ ret = true
+ end)
end
if cfg:at('symbols') then
- for k, v in cfg:at('symbols'):pairs() do
- symbol_transform(cfg, k, v)
- end
+ try_transform('symbols', function()
+ for k, v in cfg:at('symbols'):pairs() do
+ symbol_transform(cfg, k, v)
+ end
+ end)
end
check_statistics_sanity()
if not cfg:at('actions') then
logger.errx('no actions defined')
else
- -- Perform sanity check for actions
- local actions_defs = { 'no action', 'no_action', -- In case if that's added
- 'greylist', 'add header', 'add_header',
- 'rewrite subject', 'rewrite_subject', 'quarantine',
- 'reject', 'discard' }
-
- local actions = cfg:at('actions')
- if not actions:at('no action') and not actions:at('no_action') and
- not actions:at('accept') then
- for _, d in ipairs(actions_defs) do
- if actions:at(d) then
-
- local action_score
- local act = actions:at(d)
- if act:type() ~= 'object' then
- action_score = act:unwrap()
- elseif act:type() == 'object' and act:at('score') then
- action_score = act:at('score'):unwrap()
- end
+ try_transform('actions', function()
+ -- Perform sanity check for actions
+ local actions_defs = { 'no action', 'no_action', -- In case if that's added
+ 'greylist', 'add header', 'add_header',
+ 'rewrite subject', 'rewrite_subject', 'quarantine',
+ 'reject', 'discard' }
+
+ local actions = cfg:at('actions')
+ if not actions:at('no action') and not actions:at('no_action') and
+ not actions:at('accept') then
+ for _, d in ipairs(actions_defs) do
+ if actions:at(d) then
+
+ local action_score
+ local act = actions:at(d)
+ if act:type() ~= 'object' then
+ action_score = act:unwrap()
+ elseif act:type() == 'object' and act:at('score') then
+ action_score = act:at('score'):unwrap()
+ end
- if act:type() ~= 'object' and not action_score then
- actions[d] = nil
- elseif type(action_score) == 'number' and action_score < 0 then
- actions['no_action'] = actions:at(d):unwrap() - 0.001
- logger.infox(rspamd_config, 'set no_action score to: %s, as action %s has negative score',
- actions:at('no_action'):unwrap(), d)
- break
+ if act:type() ~= 'object' and not action_score then
+ actions[d] = nil
+ elseif type(action_score) == 'number' and action_score < 0 then
+ actions['no_action'] = actions:at(d):unwrap() - 0.001
+ logger.infox(rspamd_config, 'set no_action score to: %s, as action %s has negative score',
+ actions:at('no_action'):unwrap(), d)
+ break
+ end
end
end
end
- end
- local actions_set = lua_util.list_to_hash(actions_defs)
+ local actions_set = lua_util.list_to_hash(actions_defs)
- -- Now check actions section for garbage
- actions_set['unknown_weight'] = true
- actions_set['grow_factor'] = true
- actions_set['subject'] = true
+ -- Now check actions section for garbage
+ actions_set['unknown_weight'] = true
+ actions_set['grow_factor'] = true
+ actions_set['subject'] = true
- for k, v in cfg:at('actions'):pairs() do
- if not actions_set[k] then
- -- Check if this is a custom action with flags (e.g., no_threshold)
- local is_custom_action = false
- if v and v:type() == 'object' and v:at('flags') then
- is_custom_action = true
- end
- if not is_custom_action then
- logger.warnx(rspamd_config, 'unknown element in actions section: %s', k)
+ for k, v in cfg:at('actions'):pairs() do
+ if not actions_set[k] then
+ -- Check if this is a custom action with flags (e.g., no_threshold)
+ local is_custom_action = false
+ if v and v:type() == 'object' and v:at('flags') then
+ is_custom_action = true
+ end
+ if not is_custom_action then
+ logger.warnx(rspamd_config, 'unknown element in actions section: %s', k)
+ end
end
end
- end
- -- Performs thresholds sanity
- -- We exclude greylist here as it can be set to whatever threshold in practice
- local actions_order = {
- 'no_action',
- 'add_header',
- 'rewrite_subject',
- 'quarantine',
- 'reject',
- 'discard'
- }
- for i = 1, (#actions_order - 1) do
- local act = actions_order[i]
-
- if actions:at(act) and actions:at(act):type() ~= 'object' then
- local score = actions:at(act):unwrap()
-
- for j = i + 1, #actions_order do
- local next_act = actions_order[j]
- if actions:at(next_act) and actions:at(next_act):type() == 'number' then
- local next_score = actions:at(next_act):unwrap()
- if type(score) == 'number' and type(next_score) == 'number' and next_score <= score then
- logger.errx(rspamd_config, 'invalid actions thresholds order: action %s (%s) must have lower ' ..
- 'score than action %s (%s)', act, score, next_act, next_score)
- ret = false
+ -- Performs thresholds sanity
+ -- We exclude greylist here as it can be set to whatever threshold in practice
+ local actions_order = {
+ 'no_action',
+ 'add_header',
+ 'rewrite_subject',
+ 'quarantine',
+ 'reject',
+ 'discard'
+ }
+ for i = 1, (#actions_order - 1) do
+ local act = actions_order[i]
+
+ if actions:at(act) and actions:at(act):type() ~= 'object' then
+ local score = actions:at(act):unwrap()
+
+ for j = i + 1, #actions_order do
+ local next_act = actions_order[j]
+ if actions:at(next_act) and actions:at(next_act):type() == 'number' then
+ local next_score = actions:at(next_act):unwrap()
+ if type(score) == 'number' and type(next_score) == 'number' and next_score <= score then
+ logger.errx(rspamd_config, 'invalid actions thresholds order: action %s (%s) must have lower ' ..
+ 'score than action %s (%s)', act, score, next_act, next_score)
+ ret = false
+ end
end
end
end
end
- end
+ end)
end
-- DKIM signing/ARC legacy
- for _, mod in ipairs({ 'dkim_signing', 'arc' }) do
- if cfg:at(mod) then
- if cfg:at(mod):at('auth_only') then
- if cfg:at(mod):at('sign_authenticated') then
- logger.warnx(rspamd_config,
- 'both auth_only (%s) and sign_authenticated (%s) for %s are specified, prefer auth_only',
- cfg:at(mod):at('auth_only'):unwrap(), cfg:at(mod):at('sign_authenticated'):unwrap(), mod)
+ try_transform('dkim_arc_legacy', function()
+ for _, mod in ipairs({ 'dkim_signing', 'arc' }) do
+ if cfg:at(mod) then
+ if cfg:at(mod):at('auth_only') then
+ if cfg:at(mod):at('sign_authenticated') then
+ logger.warnx(rspamd_config,
+ 'both auth_only (%s) and sign_authenticated (%s) for %s are specified, prefer auth_only',
+ cfg:at(mod):at('auth_only'):unwrap(), cfg:at(mod):at('sign_authenticated'):unwrap(), mod)
+ end
+ cfg:at(mod).sign_authenticated = cfg:at(mod):at('auth_only')
end
- cfg:at(mod).sign_authenticated = cfg:at(mod):at('auth_only')
end
end
- end
- -- Deal with dkim settings
- if not cfg.dkim then
- cfg.dkim = {}
- else
- if cfg.dkim.sign_condition then
- -- We have an obsoleted sign condition, so we need to either add dkim_signing and move it
- -- there or just move sign condition there...
- if not cfg.dkim_signing then
- logger.warnx('obsoleted DKIM signing method used, converting it to "dkim_signing" module')
- cfg.dkim_signing = {
- sign_condition = cfg.dkim.sign_condition
- }
- else
- if not cfg.dkim_signing.sign_condition then
- logger.warnx('obsoleted DKIM signing method used, move it to "dkim_signing" module')
- cfg.dkim_signing.sign_condition = cfg.dkim.sign_condition
+ -- Deal with dkim settings
+ if not cfg.dkim then
+ cfg.dkim = {}
+ else
+ if cfg.dkim.sign_condition then
+ -- We have an obsoleted sign condition, so we need to either add dkim_signing and move it
+ -- there or just move sign condition there...
+ if not cfg.dkim_signing then
+ logger.warnx('obsoleted DKIM signing method used, converting it to "dkim_signing" module')
+ cfg.dkim_signing = {
+ sign_condition = cfg.dkim.sign_condition
+ }
else
- logger.warnx('obsoleted DKIM signing method used, ignore it as "dkim_signing" also defines condition!')
+ if not cfg.dkim_signing.sign_condition then
+ logger.warnx('obsoleted DKIM signing method used, move it to "dkim_signing" module')
+ cfg.dkim_signing.sign_condition = cfg.dkim.sign_condition
+ else
+ logger.warnx('obsoleted DKIM signing method used, ignore it as "dkim_signing" also defines condition!')
+ end
end
end
end
- end
+ end)
-- Try to find some obvious issues with configuration
- for k, v in cfg:pairs() do
- if v:type() == 'object' and v:at(k) and v:at(k):type() == 'object' then
- logger.errx('nested section: %s { %s { ... } }, it is likely a configuration error',
- k, k)
+ try_transform('config_sanity', function()
+ for k, v in cfg:pairs() do
+ if v:type() == 'object' and v:at(k) and v:at(k):type() == 'object' then
+ logger.errx('nested section: %s { %s { ... } }, it is likely a configuration error',
+ k, k)
+ end
end
- end
+ end)
-- If neural network is enabled we MUST have `check_all_filters` flag
if cfg:at('neural') then
-
- if cfg:at('options') then
- if not cfg:at('options'):at('check_all_filters') then
- logger.infox(rspamd_config, 'enable `options.check_all_filters` for neural network')
- cfg:at('options')['check_all_filters'] = true
+ try_transform('neural', function()
+ if cfg:at('options') then
+ if not cfg:at('options'):at('check_all_filters') then
+ logger.infox(rspamd_config, 'enable `options.check_all_filters` for neural network')
+ cfg:at('options')['check_all_filters'] = true
+ end
end
- end
+ end)
end
if cfg.surbl then
- if not cfg.rbl then
- cfg.rbl = {
- rbls = {}
- }
- end
- if not cfg.rbl.rbls then
- cfg.rbl.rbls = {}
- end
- surbl_section_convert(cfg, cfg.surbl)
- logger.infox(rspamd_config, 'converted surbl rules to rbl rules')
- cfg.surbl = nil
+ try_transform('surbl', function()
+ if not cfg.rbl then
+ cfg.rbl = {
+ rbls = {}
+ }
+ end
+ if not cfg.rbl.rbls then
+ cfg.rbl.rbls = {}
+ end
+ surbl_section_convert(cfg, cfg.surbl)
+ logger.infox(rspamd_config, 'converted surbl rules to rbl rules')
+ cfg.surbl = nil
+ end)
end
if cfg.emails then
- if not cfg.rbl then
- cfg.rbl = {
- rbls = {}
- }
- end
- if not cfg.rbl.rbls then
- cfg.rbl.rbls = {}
- end
- emails_section_convert(cfg, cfg.emails)
- logger.infox(rspamd_config, 'converted emails rules to rbl rules')
- cfg.emails = nil
+ try_transform('emails', function()
+ if not cfg.rbl then
+ cfg.rbl = {
+ rbls = {}
+ }
+ end
+ if not cfg.rbl.rbls then
+ cfg.rbl.rbls = {}
+ end
+ emails_section_convert(cfg, cfg.emails)
+ logger.infox(rspamd_config, 'converted emails rules to rbl rules')
+ cfg.emails = nil
+ end)
end
-- Common misprint options.upstreams -> options.upstream