@ -17,102 +17,30 @@ limitations under the License.
local logger = require " rspamd_logger "
local lua_util = require " lua_util "
local rspamd_util = require " rspamd_util "
local fun = require " fun "
local function is_implicit ( t )
local mt = getmetatable ( t )
return mt and mt.class and mt.class == ' ucl.type.impl_array '
end
local function metric_pairs ( t )
-- collect the keys
local keys = { }
local implicit_array = is_implicit ( t )
local function gen_keys ( tbl )
if implicit_array then
for _ , v in ipairs ( tbl ) do
if v.name then
table.insert ( keys , { v.name , v } )
v.name = nil
else
-- Very tricky to distinguish:
-- group {name = "foo" ... } + group "blah" { ... }
for gr_name , gr in pairs ( v ) do
if type ( gr_name ) ~= ' number ' then
-- We can also have implicit arrays here
local gr_implicit = is_implicit ( gr )
if gr_implicit then
for _ , gr_elt in ipairs ( gr ) do
table.insert ( keys , { gr_name , gr_elt } )
end
else
table.insert ( keys , { gr_name , gr } )
end
end
end
end
end
else
if tbl.name then
table.insert ( keys , { tbl.name , tbl } )
tbl.name = nil
else
for k , v in pairs ( tbl ) do
if type ( k ) ~= ' number ' then
-- We can also have implicit arrays here
local sym_implicit = is_implicit ( v )
if sym_implicit then
for _ , elt in ipairs ( v ) do
table.insert ( keys , { k , elt } )
end
else
table.insert ( keys , { k , v } )
end
end
end
end
end
end
gen_keys ( t )
-- return the iterator function
local i = 0
return function ( )
i = i + 1
if keys [ i ] then
return keys [ i ] [ 1 ] , keys [ i ] [ 2 ]
end
end
end
local function group_transform ( cfg , k , v )
if v.name then
k = v.name
if v : at ( ' name ' ) then
k = v : at ( ' name ' ) : unwrap ( )
end
local new_group = {
symbols = { }
}
if v.enabled then
new_group.enabled = v.enabled
if v : at ( ' enabled ' ) then
new_group.enabled = v : at ( ' enabled ' ) : unwrap ( )
end
if v.disabled then
new_group.disabled = v.disabled
if v : at ( ' disabled ' ) then
new_group.disabled = v : at ( ' disabled ' ) : unwrap ( )
end
if v.max_score then
new_group.max_score = v.max_score
new_group.max_score = v : at ( ' max_score ' ) : unwrap ( )
end
if v.symbol then
for sk , sv in metric_pairs ( v.symbol ) do
if sv.name then
sk = sv.name
if v : at ( ' symbol ' ) then
for sk , sv in v : at ( ' symbol ' ) : pairs ( ) do
if sv : at ( ' name ' ) then
sk = sv : at ( ' name ' ) : unwrap ( )
sv.name = nil -- Remove field
end
@ -120,26 +48,28 @@ local function group_transform(cfg, k, v)
end
end
if not cfg.group then
if not cfg : at ( ' group ' ) then
cfg.group = { }
end
if cfg.group [ k ] then
cfg.group [ k ] = lua_util.override_defaults ( cfg.group [ k ] , new_group )
if cfg : at ( ' group ' ) : at ( k ) then
cfg : at ( ' group ' ) [ k ] = lua_util.override_defaults ( cfg : at ( ' group ' ) [ k ] : unwrap ( ) , new_group )
else
cfg.group [ k ] = new_group
cfg : at ( ' group ' ) [ k ] = new_group
end
logger.infox ( " overriding group %s from the legacy metric settings " , k )
end
local function symbol_transform ( cfg , k , v )
local groups = cfg : at ( ' group ' )
-- first try to find any group where there is a definition of this symbol
for gr_n , gr in pairs ( cfg.group ) do
if gr.symbols and gr.symbols [ k ] then
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 )
gr. symbols[ k ] = lua_util.override_defaults ( gr.symbols [ k ] , v )
symbols [ k ] = lua_util.override_defaults ( symbols : at ( k ) : unwrap ( ) , v : unwrap ( ) )
return
end
end
@ -148,86 +78,50 @@ local function symbol_transform(cfg, k, v)
if not sym or not sym.group then
-- Otherwise we just use group 'ungrouped'
if not cfg.group . ungrouped then
cfg.group . ungrouped = {
symbols = { }
if not groups : at ( ' ungrouped ' ) then
groups.ungrouped = {
symbols = {
[ k ] = v
}
}
else
groups : at ( ' ungrouped ' ) : at ( ' symbols ' ) [ k ] = v
end
cfg.group . ungrouped.symbols [ k ] = v
logger.debugx ( " adding symbol %s to the group 'ungrouped' " , k )
end
end
local function test_groups ( groups )
for gr_name , gr in pairs ( groups ) do
if not gr.symbols then
local cnt = 0
for _ , _ in pairs ( gr ) do
cnt = cnt + 1
end
if cnt == 0 then
logger.debugx ( ' group %s is empty ' , gr_name )
else
logger.infox ( ' group %s has no symbols ' , gr_name )
end
end
local function convert_metric ( cfg , metric )
if metric : type ( ) ~= ' object ' then
logger.errx ( ' invalid metric definition: %s ' , metric )
return
end
end
local function convert_metric ( cfg , metric )
if metric.actions then
cfg.actions = lua_util.override_defaults ( cfg.actions , metric.actions )
if metric : at ( ' actions ' ) then
cfg.actions = lua_util.override_defaults ( cfg : at ( ' actions ' ) : unwrap ( ) , metric : at ( ' actions ' ) : unwrap ( ) )
logger.infox ( " overriding actions from the legacy metric settings " )
end
if metric.unknown_weight then
cfg.actions . unknown_weight = metric.unknown_weight
if metric : at ( ' unknown_weight ' ) then
cfg : at ( ' actions ' ) . unknown_weight = metric : at ( ' unknown_weight ' ) : unwrap ( )
end
if metric.subject then
if metric : at ( ' subject ' ) then
logger.infox ( " overriding subject from the legacy metric settings " )
cfg.actions . subject = metric.subject
cfg : at ( ' actions ' ) . subject = metric : at ( ' subject ' ) : unwrap ( )
end
if metric.group then
for k , v in metric_pairs ( metric.group ) do
if metric : at ( ' group ' ) then
for k , v in metric : at ( ' group ' ) : pairs ( ) do
group_transform ( cfg , k , v )
end
else
if not cfg.group then
cfg.group = {
ungrouped = {
symbols = { }
}
}
end
end
if metric.symbol then
for k , v in metric_pairs ( metric.symbol ) do
if metric : at ( ' symbol ' ) then
for k , v in metric : at ( ' symbol ' ) : pairs ( ) do
symbol_transform ( cfg , k , v )
end
end
return cfg
end
-- Converts a table of groups indexed by number (implicit array) to a
-- merged group definition
local function merge_groups ( groups )
local ret = { }
for k , gr in pairs ( groups ) do
if type ( k ) == ' number ' then
for key , sec in pairs ( gr ) do
ret [ key ] = sec
end
else
ret [ k ] = gr
end
end
return ret
end
-- Checks configuration files for statistics
@ -245,155 +139,27 @@ local function check_statistics_sanity()
end
end
-- Converts surbl module config to rbl module
local function surbl_section_convert ( cfg , section )
local rbl_section = cfg.rbl . rbls
local wl = section.whitelist
for name , value in pairs ( section.rules or { } ) do
if rbl_section [ name ] then
logger.warnx ( rspamd_config , ' conflicting names in surbl and rbl rules: %s, prefer surbl rule! ' ,
name )
end
local converted = {
urls = true ,
ignore_defaults = true ,
}
if wl then
converted.whitelist = wl
end
for k , v in pairs ( value ) do
local skip = false
-- Rename
if k == ' suffix ' then
k = ' rbl '
end
if k == ' ips ' then
k = ' returncodes '
end
if k == ' bits ' then
k = ' returnbits '
end
if k == ' noip ' then
k = ' no_ip '
end
-- Crappy legacy
if k == ' options ' then
if v == ' noip ' or v == ' no_ip ' then
converted.no_ip = true
skip = true
end
end
if k : match ( ' check_ ' ) then
local n = k : match ( ' check_(.*) ' )
k = n
end
if k == ' dkim ' and v then
converted.dkim_domainonly = false
converted.dkim_match_from = true
end
if k == ' emails ' and v then
-- To match surbl behaviour
converted.emails_domainonly = true
end
if not skip then
converted [ k ] = lua_util.deepcopy ( v )
end
end
rbl_section [ name ] = lua_util.override_defaults ( rbl_section [ name ] , converted )
end
end
-- Converts surbl module config to rbl module
local function emails_section_convert ( cfg , section )
local rbl_section = cfg.rbl . rbls
local wl = section.whitelist
for name , value in pairs ( section.rules or { } ) do
if rbl_section [ name ] then
logger.warnx ( rspamd_config , ' conflicting names in emails and rbl rules: %s, prefer emails rule! ' ,
name )
end
local converted = {
emails = true ,
ignore_defaults = true ,
}
if wl then
converted.whitelist = wl
end
for k , v in pairs ( value ) do
local skip = false
-- Rename
if k == ' dnsbl ' then
k = ' rbl '
end
if k == ' check_replyto ' then
k = ' replyto '
end
if k == ' hashlen ' then
k = ' hash_len '
end
if k == ' encoding ' then
k = ' hash_format '
end
if k == ' domain_only ' then
k = ' emails_domainonly '
end
if k == ' delimiter ' then
k = ' emails_delimiter '
end
if k == ' skip_body ' then
skip = true
if v then
-- Hack
converted.emails = false
converted.replyto = true
else
converted.emails = true
end
end
if k == ' expect_ip ' then
-- Another stupid hack
if not converted.return_codes then
converted.returncodes = { }
end
local symbol = value.symbol or name
converted.returncodes [ symbol ] = { v }
skip = true
end
if not skip then
converted [ k ] = lua_util.deepcopy ( v )
end
end
rbl_section [ name ] = lua_util.override_defaults ( rbl_section [ name ] , converted )
end
end
return function ( cfg )
local ret = false
if cfg [ ' metric ' ] then
for _ , v in metric_pairs ( cfg.metric ) do
cfg = convert_metric ( cfg , v )
if cfg : at ( ' metric ' ) then
for _ , v in cfg : at ( ' metric ' ) : pairs ( ) do
if v : type ( ) == ' object ' then
convert_metric ( cfg , v )
end
end
ret = true
end
if cfg.symbols then
for k , v in metric_pairs ( cfg.symbols ) do
if cfg : at ( ' symbols ' ) then
for k , v in cfg : at ( ' symbols ' ) : pairs ( ) do
symbol_transform ( cfg , k , v )
end
end
check_statistics_sanity ( )
if not cfg.actions then
if not cfg : at ( ' actions ' ) then
logger.errx ( ' no actions defined ' )
else
-- Perform sanity check for actions
@ -402,24 +168,26 @@ return function(cfg)
' rewrite subject ' , ' rewrite_subject ' , ' quarantine ' ,
' reject ' , ' discard ' }
if not cfg.actions [ ' no action ' ] and not cfg.actions [ ' no_action ' ] and
not cfg.actions [ ' accept ' ] then
local actions = cfg : at ( ' actions ' )
if actions and ( 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 cfg.actions [ d ] then
local action_score = nil
if type ( cfg.actions [ d ] ) == ' number ' then
action_score = cfg.actions [ d ]
elseif type ( cfg.actions [ d ] ) == ' table ' and cfg.actions [ d ] [ ' score ' ] then
action_score = cfg.actions [ d ] [ ' score ' ]
if actions : at ( d ) then
local action_score
local act = actions : at ( d )
if act : type ( ) == ' number ' then
action_score = act : unwrap ( )
elseif act : type ( ) == ' object ' and act : at ( ' score ' ) then
action_score = act : at ( ' score ' ) : unwrap ( )
end
if type ( cfg.actions [ d ] ) ~= ' table ' and not action_score then
cfg. actions[ d ] = nil
if act : type ( ) ~= ' object ' and not action_score then
actions [ d ] = nil
elseif type ( action_score ) == ' number ' and action_score < 0 then
cfg. actions[ ' no_action ' ] = cfg.actions [ d ] - 0.001
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 ' ,
cfg.actions [ ' no_action ' ] , d )
actions : at ( ' no_action ' ) : unwrap ( ) , d )
break
end
end
@ -433,7 +201,7 @@ return function(cfg)
actions_set [ ' grow_factor ' ] = true
actions_set [ ' subject ' ] = true
for k , _ in pairs ( cfg.actions ) do
for k , _ in cfg : at ( ' actions ' ) : pairs ( ) do
if not actions_set [ k ] then
logger.warnx ( rspamd_config , ' unknown element in actions section: %s ' , k )
end
@ -452,13 +220,13 @@ return function(cfg)
for i = 1 , ( # actions_order - 1 ) do
local act = actions_order [ i ]
if cfg.actions [ act ] and type ( cfg.actions [ act ] ) == ' number ' then
local score = cfg.actions [ act ]
if actions : at ( act ) and actions : at ( act ) : type ( ) == ' number ' then
local score = actions : at ( act ) : unwrap ( )
for j = i + 1 , # actions_order do
local next_act = actions_order [ j ]
if cfg.actions [ next_act ] and type ( cfg.actions [ next_act ] ) == ' number ' then
local next_score = cfg.actions [ next_act ]
if actions : at ( next_act ) and actions : at ( next_act ) : type ( ) == ' number ' then
local next_score = actions : at ( next_act ) : unwrap ( )
if 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 )
@ -470,164 +238,35 @@ return function(cfg)
end
end
if not cfg.group then
logger.errx ( ' no symbol groups defined ' )
else
if cfg.group [ 1 ] then
-- We need to merge groups
cfg.group = merge_groups ( cfg.group )
ret = true
end
test_groups ( cfg.group )
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
else
logger.warnx ( ' obsoleted DKIM signing method used, ignore it as "dkim_signing" also defines condition! ' )
end
end
end
end
-- Again: legacy stuff :(
if not cfg.dkim . sign_headers then
local sec = cfg.dkim_signing
if sec and sec [ 1 ] then
sec = cfg.dkim_signing [ 1 ]
end
if sec and sec.sign_headers then
cfg.dkim . sign_headers = sec.sign_headers
end
end
-- DKIM signing/ARC legacy
for _ , mod in ipairs ( { ' dkim_signing ' , ' arc ' } ) do
if cfg [ mod ] then
if cfg [ mod ] . auth_only ~= nil then
if cfg [ mod ] . sign_authenticated ~= nil then
if cfg : at ( mod ) then
if cfg : at ( mod ) : at ( ' auth_only ' ) : unwrap ( ) ~= nil then
if cfg : at ( mod ) : at ( ' sign_authenticated ' ) : unwrap ( ) ~= nil then
logger.warnx ( rspamd_config ,
' both auth_only (%s) and sign_authenticated (%s) for %s are specified, prefer auth_only ' ,
cfg [ mod ] . auth_only , cfg [ mod ] . sign_authenticated , mod )
cfg : at ( mod ) : at ( ' auth_only ' ) : unwrap ( ) , cfg : at ( mod ) : at ( ' sign_authenticated ' ) : unwrap ( ) , mod )
end
cfg [ mod ] . sign_authenticated = cfg [ mod ] . auth_only
cfg : at ( mod ) . sign_authenticated = cfg : at ( mod ) : at ( ' auth_only ' )
end
end
end
if cfg.dkim and cfg.dkim . sign_headers and type ( cfg.dkim . sign_headers ) == ' table ' then
-- Flatten
cfg.dkim . sign_headers = table.concat ( cfg.dkim . sign_headers , ' : ' )
end
-- Try to find some obvious issues with configuration
for k , v in pairs ( cfg ) do
if type ( v ) == ' table ' and v [ k ] and type ( v [ k ] ) == ' table ' then
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
-- If neural network is enabled we MUST have `check_all_filters` flag
if cfg.neural then
if not cfg.options then
cfg.options = { }
end
if cfg : at ( ' neural ' ) then
if not cfg.options . check_all_filters then
if not cfg : at ( ' options ' ) : at ( ' check_all_filters ' ) then
logger.infox ( rspamd_config , ' enable `options.check_all_filters` for neural network ' )
cfg.options . check_all_filters = true
end
end
-- Deal with IP_SCORE
if cfg.ip_score and ( cfg.ip_score . servers or cfg.redis . servers ) then
logger.warnx ( rspamd_config , ' ip_score module is deprecated in honor of reputation module! ' )
if not cfg.reputation then
cfg.reputation = {
rules = { }
}
end
if not cfg.reputation . rules then
cfg.reputation . rules = { }
end
if not fun.any ( function ( _ , v )
return v.selector and v.selector . ip
end ,
cfg.reputation . rules ) then
logger.infox ( rspamd_config , ' attach ip reputation element to use it ' )
cfg.reputation . rules.ip_score = {
selector = {
ip = { } ,
} ,
backend = {
redis = { } ,
}
}
if cfg.ip_score . servers then
cfg.reputation . rules.ip_score . backend.redis . servers = cfg.ip_score . servers
end
if cfg.symbols and cfg.symbols [ ' IP_SCORE ' ] then
local t = cfg.symbols [ ' IP_SCORE ' ]
if not cfg.symbols [ ' SENDER_REP_SPAM ' ] then
cfg.symbols [ ' SENDER_REP_SPAM ' ] = t
cfg.symbols [ ' SENDER_REP_HAM ' ] = t
cfg.symbols [ ' SENDER_REP_HAM ' ] . weight = - ( t.weight or 0 )
end
end
else
logger.infox ( rspamd_config , ' ip reputation already exists, do not do any IP_SCORE transforms ' )
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 = { }
end
if cfg.emails then
if not cfg.rbl then
cfg.rbl = {
rbls = { }
}
end
if not cfg.rbl . rbls then
cfg.rbl . rbls = { }
cfg : at ( ' options ' ) [ ' check_all_filters ' ] = true
end
emails_section_convert ( cfg , cfg.emails )
logger.infox ( rspamd_config , ' converted emails rules to rbl rules ' )
cfg.emails = { }
end
-- Common misprint options.upstreams -> options.upstream