local fun = require "fun" local rspamd_logger = require "rspamd_logger" local util = require "rspamd_util" local lua_util = require "lua_util" local rspamd_regexp = require "rspamd_regexp" local ucl = require "ucl" local complicated = {} local rules = {} local scores = {} local function words_to_re(words, start) return table.concat(fun.totable(fun.drop_n(start, words)), " "); end local function split(str, delim) local result = {} if not delim then delim = '[^%s]+' end for token in string.gmatch(str, delim) do table.insert(result, token) end return result end local function handle_header_def(hline, cur_rule) --Now check for modifiers inside header's name local hdrs = split(hline, '[^|]+') local hdr_params = {} local cur_param = {} -- Check if an re is an ordinary re local ordinary = true for _,h in ipairs(hdrs) do if h == 'ALL' or h == 'ALL:raw' then ordinary = false else local args = split(h, '[^:]+') cur_param['strong'] = false cur_param['raw'] = false cur_param['header'] = args[1] if args[2] then -- We have some ops that are required for the header, so it's not ordinary ordinary = false end fun.each(function(func) if func == 'addr' then cur_param['function'] = function(str) local addr_parsed = util.parse_mail_address(str) local ret = {} if addr_parsed then for _,elt in ipairs(addr_parsed) do if elt['addr'] then table.insert(ret, elt['addr']) end end end return ret end elseif func == 'name' then cur_param['function'] = function(str) local addr_parsed = util.parse_mail_address(str) local ret = {} if addr_parsed then for _,elt in ipairs(addr_parsed) do if elt['name'] then table.insert(ret, elt['name']) end end end return ret end elseif func == 'raw' then cur_param['raw'] = true elseif func == 'case' then cur_param['strong'] = true else rspamd_logger.warnx(rspamd_config, 'Function %1 is not supported in %2', func, cur_rule['symbol']) end end, fun.tail(args)) -- Some header rules require splitting to check of multiple headers if cur_param['header'] == 'MESSAGEID' then -- Special case for spamassassin ordinary = false elseif cur_param['header'] == 'ToCc' then ordinary = false else table.insert(hdr_params, cur_param) end end cur_rule['ordinary'] = ordinary and #hdr_params <= 1 cur_rule['header'] = hdr_params end end local function process_sa_conf(f) local cur_rule = {} local valid_rule = false local function insert_cur_rule() if not rules[cur_rule.type] then rules[cur_rule.type] = {} end local target = rules[cur_rule.type] if cur_rule.type == 'header' then if not cur_rule.header[1].header then rspamd_logger.errx(rspamd_config, 'bad rule definition: %1', cur_rule) return end if not target[cur_rule.header[1].header] then target[cur_rule.header[1].header] = {} end target = target[cur_rule.header[1].header] end if not cur_rule['symbol'] then rspamd_logger.errx(rspamd_config, 'bad rule definition: %1', cur_rule) return end target[cur_rule['symbol']] = cur_rule cur_rule = {} valid_rule = false end local function parse_score(words) if #words == 3 then -- score rule return tonumber(words[3]) elseif #words == 6 then -- score rule -- we assume here that bayes and network are enabled and select return tonumber(words[6]) else rspamd_logger.errx(rspamd_config, 'invalid score for %1', words[2]) end return 0 end local skip_to_endif = false local if_nested = 0 for l in f:lines() do (function () l = lua_util.rspamd_str_trim(l) -- Replace bla=~/re/ with bla =~ /re/ (#2372) l = l:gsub('([^%s])%s*([=!]~)%s*([^%s])', '%1 %2 %3') if string.len(l) == 0 or string.sub(l, 1, 1) == '#' then return end -- Unbalanced if/endif if if_nested < 0 then if_nested = 0 end if skip_to_endif then if string.match(l, '^endif') then if_nested = if_nested - 1 if if_nested == 0 then skip_to_endif = false end elseif string.match(l, '^if') then if_nested = if_nested + 1 elseif string.match(l, '^else') then -- Else counterpart for if skip_to_endif = false end table.insert(complicated, l) return else if string.match(l, '^ifplugin') then skip_to_endif = true if_nested = if_nested + 1 table.insert(complicated, l) elseif string.match(l, '^if !plugin%(') then skip_to_endif = true if_nested = if_nested + 1 table.insert(complicated, l) elseif string.match(l, '^if') then -- Unknown if skip_to_endif = true if_nested = if_nested + 1 table.insert(complicated, l) elseif string.match(l, '^else') then -- Else counterpart for if skip_to_endif = true table.insert(complicated, l) elseif string.match(l, '^endif') then if_nested = if_nested - 1 table.insert(complicated, l) end end -- Skip comments local words = fun.totable(fun.take_while( function(w) return string.sub(w, 1, 1) ~= '#' end, fun.filter(function(w) return w ~= "" end, fun.iter(split(l))))) if words[1] == "header" then -- header SYMBOL Header ~= /regexp/ if valid_rule then insert_cur_rule() end if words[4] and (words[4] == '=~' or words[4] == '!~') then cur_rule['type'] = 'header' cur_rule['symbol'] = words[2] if words[4] == '!~' then table.insert(complicated, l) return end cur_rule['re_expr'] = words_to_re(words, 4) local unset_comp = string.find(cur_rule['re_expr'], '%s+%[if%-unset:') if unset_comp then table.insert(complicated, l) return end cur_rule['re'] = rspamd_regexp.create(cur_rule['re_expr']) if not cur_rule['re'] then rspamd_logger.warnx(rspamd_config, "Cannot parse regexp '%1' for %2", cur_rule['re_expr'], cur_rule['symbol']) table.insert(complicated, l) return else handle_header_def(words[3], cur_rule) if not cur_rule['ordinary'] then table.insert(complicated, l) return end end valid_rule = true else table.insert(complicated, l) return end elseif words[1] == "body" then -- body SYMBOL /regexp/ if valid_rule then insert_cur_rule() end cur_rule['symbol'] = words[2] if words[3] and (string.sub(words[3], 1, 1) == '/' or string.sub(words[3], 1, 1) == 'm') then cur_rule['type'] = 'sabody' cur_rule['re_expr'] = words_to_re(words, 2) cur_rule['re'] = rspamd_regexp.create(cur_rule['re_expr']) if cur_rule['re'] then valid_rule = true end else -- might be function table.insert(complicated, l) return end elseif words[1] == "rawbody" then -- body SYMBOL /regexp/ if valid_rule then insert_cur_rule() end cur_rule['symbol'] = words[2] if words[3] and (string.sub(words[3], 1, 1) == '/' or string.sub(words[3], 1, 1) == 'm') then cur_rule['type'] = 'sarawbody' cur_rule['re_expr'] = words_to_re(words, 2) cur_rule['re'] = rspamd_regexp.create(cur_rule['re_expr']) if cur_rule['re'] then valid_rule = true end else table.insert(complicated, l) return end elseif words[1] == "full" then -- body SYMBOL /regexp/ if valid_rule then insert_cur_rule() end cur_rule['symbol'] = words[2] if words[3] and (string.sub(words[3], 1, 1) == '/' or string.sub(words[3], 1, 1) == 'm') then cur_rule['type'] = 'message' cur_rule['re_expr'] = words_to_re(words, 2) cur_rule['re'] = rspamd_regexp.create(cur_rule['re_expr']) cur_rule['raw'] = true if cur_rule['re'] then valid_rule = true end else table.insert(complicated, l) return end elseif words[1] == "uri" then -- uri SYMBOL /regexp/ if valid_rule then insert_cur_rule() end cur_rule['type'] = 'uri' cur_rule['symbol'] = words[2] cur_rule['re_expr'] = words_to_re(words, 2) cur_rule['re'] = rspamd_regexp.create(cur_rule['re_expr']) if cur_rule['re'] and cur_rule['symbol'] then valid_rule = true else table.insert(complicated, l) return end elseif words[1] == "meta" then -- meta SYMBOL expression if valid_rule then insert_cur_rule() end table.insert(complicated, l) return elseif words[1] == "describe" and valid_rule then cur_rule['description'] = words_to_re(words, 2) elseif words[1] == "score" then scores[words[2]] = parse_score(words) else table.insert(complicated, l) return end end)() end if valid_rule then insert_cur_rule() end end for _,matched in ipairs(arg) do local f = io.open(matched, "r") if f then rspamd_logger.messagex(rspamd_config, 'loading SA rules from %s', matched) process_sa_conf(f) else rspamd_logger.errx(rspamd_config, "cannot open %1", matched) end end local multimap_conf = {} local function handle_rule(what, syms, hdr) local mtype local filter local fname local header local sym = what:upper() if what == 'sabody' then mtype = 'content' fname = 'body_re.map' filter = 'oneline' elseif what == 'sarawbody' then fname = 'raw_body_re.map' mtype = 'content' filter = 'rawtext' elseif what == 'full' then fname = 'full_re.map' mtype = 'content' filter = 'full' elseif what == 'uri' then fname = 'uri_re.map' mtype = 'url' filter = 'full' elseif what == 'header' then fname = ('hdr_' .. hdr .. '_re.map'):lower() mtype = 'header' header = hdr sym = sym .. '_' .. hdr:upper() else rspamd_logger.errx('unknown type: %s', what) return end local conf = { type = mtype, filter = filter, symbol = 'SA_MAP_AUTO_' .. sym, regexp = true, map = fname, header = header, symbols = {} } local re_file = io.open(fname, 'w') for k,r in pairs(syms) do local score = 0.0 if scores[k] then score = scores[k] end re_file:write(string.format('/%s/ %s:%f\n', tostring(r.re), k, score)) table.insert(conf.symbols, k) end re_file:close() multimap_conf[sym:lower()] = conf rspamd_logger.messagex('stored %s regexp in %s', sym:lower(), fname) end for k,v in pairs(rules) do if k == 'header' then for h,r in pairs(v) do handle_rule(k, r, h) end else handle_rule(k, v) end end local out = ucl.to_format(multimap_conf, 'ucl') local mmap_conf = io.open('auto_multimap.conf', 'w') mmap_conf:write(out) mmap_conf:close() rspamd_logger.messagex('stored multimap conf in %s', 'auto_multimap.conf') local sa_remain = io.open('auto_sa.conf', 'w') fun.each(function(l) sa_remain:write(l) sa_remain:write('\n') end, fun.filter(function(l) return not string.match(l, '^%s+$') end, complicated)) sa_remain:close() rspamd_logger.messagex('stored sa remains conf in %s', 'auto_sa.conf')