]> git.ipfire.org Git - thirdparty/rspamd.git/commitdiff
[Feature] Allow selectors in regexp maps expressions
authorVsevolod Stakhov <vsevolod@rspamd.com>
Fri, 12 Sep 2025 11:26:12 +0000 (12:26 +0100)
committerVsevolod Stakhov <vsevolod@rspamd.com>
Fri, 12 Sep 2025 11:26:12 +0000 (12:26 +0100)
src/lua/lua_config.c
src/lua/lua_task.c
src/plugins/lua/multimap.lua

index 7e8ee39f2bc09c2a19a80280c4d82f28a70e0c63..215054b1ca5071dbd5959be9a6d0b53538f11b55 100644 (file)
@@ -581,7 +581,9 @@ LUA_FUNCTION_DEF(config, replace_regexp);
  *   + `rawheader`: raw header expression
  *   + `body`: raw body regexp
  *   + `url`: url regexp
+ *   + `selector`: selector regexp
  * - `header`: for header and rawheader regexp means the name of header
+ * - `selector`: for selector regexp means selector name (registered in scope)
  * - `pcre_only`: flag regexp as pcre only regexp
  * @param {string} scope scope name for the regexp
  * @param {table} params regexp parameters
@@ -5012,8 +5014,7 @@ lua_config_register_regexp_scoped(lua_State *L)
        const char *scope = luaL_checkstring(L, 2);
        struct rspamd_lua_regexp *re = NULL;
        rspamd_regexp_t *cache_re;
-       const char *type_str = NULL, *header_str = NULL;
-       gsize header_len = 0;
+       const char *type_str = NULL, *header_str = NULL, *selector_str = NULL;
        GError *err = NULL;
        enum rspamd_re_type type = RSPAMD_RE_BODY;
        gboolean pcre_only = FALSE;
@@ -5021,21 +5022,23 @@ lua_config_register_regexp_scoped(lua_State *L)
        /*
         * - `scope`*: scope name for the regexp
         * - `re`* : regular expression object
-        * - `type`*: type of regular expression:
+        * - `type`*: type of regular expression:
         *   + `mime`: mime regexp
         *   + `rawmime`: raw mime regexp
         *   + `header`: header regexp
         *   + `rawheader`: raw header expression
         *   + `body`: raw body regexp
         *   + `url`: url regexp
+        *   + `selector`: selector regexp
         * - `header`: for header and rawheader regexp means the name of header
+        * - `selector`: for selector regexp means selector name (registered in scope)
         * - `pcre_only`: allow merely pcre for this regexp
         */
        if (cfg != NULL && scope != NULL) {
                if (!rspamd_lua_parse_table_arguments(L, 3, &err,
                                                                                          RSPAMD_LUA_PARSE_ARGUMENTS_DEFAULT,
-                                                                                         "*re=U{regexp};*type=S;header=S;pcre_only=B",
-                                                                                         &re, &type_str, &header_str, &pcre_only)) {
+                                                                                         "*re=U{regexp};*type=S;header=S;selector=S;pcre_only=B",
+                                                                                         &re, &type_str, &header_str, &selector_str, &pcre_only)) {
                        msg_err_config("cannot get parameters list: %e", err);
 
                        if (err) {
@@ -5058,13 +5061,22 @@ lua_config_register_regexp_scoped(lua_State *L)
                                                                                        rspamd_regexp_get_flags(re->re) | RSPAMD_REGEXP_FLAG_PCRE_ONLY);
                                }
 
-                               if (header_str != NULL) {
+                               const char *type_data = NULL;
+                               gsize type_len = 0;
+
+                               if (header_str != NULL &&
+                                       (type == RSPAMD_RE_HEADER || type == RSPAMD_RE_RAWHEADER || type == RSPAMD_RE_MIMEHEADER)) {
                                        /* Include the last \0 */
-                                       header_len = strlen(header_str) + 1;
+                                       type_len = strlen(header_str) + 1;
+                                       type_data = header_str;
+                               }
+                               else if (selector_str != NULL && type == RSPAMD_RE_SELECTOR) {
+                                       type_len = strlen(selector_str) + 1;
+                                       type_data = selector_str;
                                }
 
                                cache_re = rspamd_re_cache_add_scoped(&cfg->re_cache, scope, re->re, type,
-                                                                                                         (gpointer) header_str, header_len, -1);
+                                                                                                         (gpointer) type_data, type_len, -1);
 
                                /*
                                 * XXX: here are dragons!
index 0b1473b61c5b9f098f6c0395fdeec604933fc0df..5085e6c2abe79e261405811fa22ab691b1896eff 100644 (file)
@@ -6293,29 +6293,33 @@ lua_task_process_regexp(lua_State *L)
        struct rspamd_task *task = lua_check_task(L, 1);
        struct rspamd_lua_regexp *re = NULL;
        gboolean strong = FALSE;
-       const char *type_str = NULL, *header_str = NULL;
-       gsize header_len = 0;
+       const char *type_str = NULL, *header_str = NULL, *selector_str = NULL;
+       gsize header_len = 0, selector_len = 0;
        GError *err = NULL;
        int ret = 0;
        enum rspamd_re_type type = RSPAMD_RE_BODY;
 
        /*
         * - `re`* : regular expression object
-        * - `type`*: type of regular expression:
+        * - `type`*: type of regular expression:
         *   + `mime`: mime regexp
         *   + `rawmime`: raw mime regexp
         *   + `header`: header regexp
         *   + `rawheader`: raw header expression
         *   + `body`: raw body regexp
         *   + `url`: url regexp
-        * - `header`: for header and rawheader regexp means the name of header
+        *   + `selector`: selector regexp
+        * - `header`: for header/rawheader/mimeheader regexp means the name of header
+        * - `selector`: for selector regexp means the selector name (registered in scope)
         * - `strong`: case sensitive match for headers
         */
        if (task != NULL) {
                if (!rspamd_lua_parse_table_arguments(L, 2, &err,
                                                                                          RSPAMD_LUA_PARSE_ARGUMENTS_DEFAULT,
-                                                                                         "*re=U{regexp};*type=S;header=V;strong=B",
-                                                                                         &re, &type_str, &header_len, &header_str,
+                                                                                         "*re=U{regexp};*type=S;header=V;selector=V;strong=B",
+                                                                                         &re, &type_str,
+                                                                                         &header_len, &header_str,
+                                                                                         &selector_len, &selector_str,
                                                                                          &strong)) {
                        msg_err_task("cannot get parameters list: %e", err);
 
@@ -6328,13 +6332,30 @@ lua_task_process_regexp(lua_State *L)
                else {
                        type = rspamd_re_cache_type_from_string(type_str);
 
-                       if ((type == RSPAMD_RE_HEADER || type == RSPAMD_RE_RAWHEADER) && header_str == NULL) {
+                       if ((type == RSPAMD_RE_HEADER || type == RSPAMD_RE_RAWHEADER || type == RSPAMD_RE_MIMEHEADER) && header_str == NULL) {
                                msg_err_task(
                                        "header argument is mandatory for header/rawheader regexps");
                        }
                        else {
+                               const char *type_data = NULL;
+                               gsize type_len = 0;
+
+                               if (type == RSPAMD_RE_HEADER || type == RSPAMD_RE_RAWHEADER || type == RSPAMD_RE_MIMEHEADER) {
+                                       type_data = header_str;
+                                       type_len = header_len;
+                               }
+                               else if (type == RSPAMD_RE_SELECTOR) {
+                                       if (selector_str == NULL) {
+                                               msg_err_task("selector argument is mandatory for selector regexps");
+                                       }
+                                       else {
+                                               type_data = selector_str;
+                                               type_len = selector_len;
+                                       }
+                               }
+
                                ret = rspamd_re_cache_process(task, re->re, type,
-                                                                                         (gpointer) header_str, header_len, strong);
+                                                                                         (gpointer) type_data, type_len, strong);
                        }
                }
        }
index 8bb62bef1392f65fafbd4362bf787da78b0907a5..7e93ba058fcaef395404520837c475dd21936ac8 100644 (file)
@@ -62,8 +62,8 @@ local function parse_multimap_value(parse_rule, p_ret)
           (digit ^ 1)
 
       -- Matches: 55.97, -90.8, .9
-      number.decimal = (number.integer * -- Integer
-          (number.fractional ^ -1)) + -- Fractional
+      number.decimal = (number.integer *     -- Integer
+            (number.fractional ^ -1)) +      -- Fractional
           (lpeg.S("+-") * number.fractional) -- Completely fractional number
 
       local sym_start = lpeg.R("az", "AZ") + lpeg.S("_")
@@ -98,7 +98,7 @@ local function parse_multimap_value(parse_rule, p_ret)
     else
       if p_ret ~= '' then
         rspamd_logger.infox(rspamd_config, '%s: cannot parse string "%s"',
-            parse_rule.symbol, p_ret)
+          parse_rule.symbol, p_ret)
       end
 
       return true, nil, 1.0, {}
@@ -183,6 +183,19 @@ local function create_sa_atom_function(name, re, match_type, opts)
       ret = process_re_match(re, task, 'body')
     elseif match_type == 'uri' then
       ret = process_re_match(re, task, 'url')
+    elseif match_type == 'selector' then
+      -- For selector regexps, use structured call with explicit selector field
+      local params = {
+        re = re,
+        type = 'selector',
+        selector = opts.selector,
+        strong = false,
+      }
+      if type(jit) == 'table' then
+        ret = task:process_regexp(params)
+      else
+        ret = task:process_regexp(params)
+      end
     else
       -- Default to body
       ret = process_re_match(re, task, 'sabody')
@@ -259,7 +272,7 @@ local function process_sa_line(rule, line)
         }
 
         lua_util.debugm(N, rspamd_config, 'added SA header atom: %s for header %s (scope: %s)',
-            atom_name, header_name, scope_name)
+          atom_name, header_name, scope_name)
       end
     end
   elseif words[1] == 'body' then
@@ -378,6 +391,69 @@ local function process_sa_line(rule, line)
         lua_util.debugm(N, rspamd_config, 'added SA full atom: %s (scope: %s)', atom_name, scope_name)
       end
     end
+  elseif words[1] == 'selector' then
+    -- selector SYMBOL selector_pipeline =~ /regexp/flags
+    -- selector pipeline can contain spaces; find operator position first
+    if #words >= 4 then
+      local atom_name = words[2]
+      local op_idx = nil
+      for i = 4, #words do
+        if words[i] == '=~' or words[i] == '!~' then
+          op_idx = i
+          break
+        end
+      end
+      if not op_idx then
+        return
+      end
+      local selector_pipeline_tbl = {}
+      for i = 3, op_idx - 1 do
+        selector_pipeline_tbl[#selector_pipeline_tbl + 1] = words[i]
+      end
+      local selector_pipeline = table.concat(selector_pipeline_tbl, ' ')
+      local re_expr = words_to_sa_re(words, op_idx)
+
+      -- Skip =~ or !~
+      re_expr = string.gsub(re_expr, '^[!=]~%s*', '')
+
+      local re = parse_sa_regexp(atom_name, re_expr)
+      if re then
+        -- Register selector and regexp in cache scope to use regexp-cache + hyperscan
+        local ok = rspamd_config:register_re_selector_scoped(scope_name, atom_name, selector_pipeline, "", false)
+        if not ok then
+          rspamd_logger.errx(rspamd_config, 'selector atom %s has invalid selector (registration failed): %s',
+            atom_name, selector_pipeline)
+          return
+        end
+
+        rspamd_config:register_regexp_scoped(scope_name, {
+          re = re,
+          type = 'selector',
+          selector = atom_name,
+          pcre_only = false,
+        })
+
+        re:set_limit(0)
+        re:set_max_hits(1)
+
+        local negate = (words[op_idx] == '!~')
+        sa_atoms[atom_name] = create_sa_atom_function(atom_name, re, 'selector', {
+          selector = atom_name,
+          strong = false,
+          negate = negate,
+        })
+
+        -- Track atom state consistent with scoped regexps
+        regexp_rules_symbol_states[atom_name] = {
+          state = 'loading',
+          rule_name = rule_name,
+          type = 'atom'
+        }
+
+        lua_util.debugm(N, rspamd_config, 'added SA selector atom: %s for selector %s (scope: %s)',
+          atom_name, selector_pipeline, scope_name)
+      end
+    end
   elseif words[1] == 'meta' then
     -- meta SYMBOL expression
     if #words >= 3 then
@@ -444,10 +520,14 @@ local function gen_sa_process_atom_cb(task, rule_name)
       if state_info.state == 'orphaned' or state_info.state == 'loading' then
         -- Double-check by looking at scope loaded state
         local scope_loaded = false
-        for _, rule in ipairs(rules) do
-          if rule.symbol == state_info.rule_name and rule.scope_name then
-            scope_loaded = rspamd_config:is_regexp_scope_loaded(rule.scope_name)
-            break
+        if state_info.scope_optional then
+          scope_loaded = true
+        else
+          for _, rule in ipairs(rules) do
+            if rule.symbol == state_info.rule_name and rule.scope_name then
+              scope_loaded = rspamd_config:is_regexp_scope_loaded(rule.scope_name)
+              break
+            end
           end
         end
 
@@ -455,7 +535,7 @@ local function gen_sa_process_atom_cb(task, rule_name)
           -- Update state to available if scope is loaded and atom exists
           state_info.state = 'available'
           lua_util.debugm(N, task, 'regexp_rules atom %s was %s, but scope is loaded - marking as available',
-              atom, state_info.state)
+            atom, state_info.state)
         else
           lua_util.debugm(N, task, 'regexp_rules atom %s is %s, returning 0', atom, state_info.state)
           return 0
@@ -507,10 +587,10 @@ create_sa_meta_callback = function(meta_rule)
           -- Update state to available if scope is loaded and meta rule exists
           state_info.state = 'available'
           lua_util.debugm(N, task, 'regexp_rules meta %s was %s, but scope is loaded - marking as available',
-              meta_rule.symbol, state_info.state)
+            meta_rule.symbol, state_info.state)
         else
           lua_util.debugm(N, task, 'regexp_rules meta %s is %s, skipping execution',
-              meta_rule.symbol, state_info.state)
+            meta_rule.symbol, state_info.state)
           return 0
         end
       end
@@ -532,8 +612,8 @@ create_sa_meta_callback = function(meta_rule)
 
     if not (already_processed and already_processed['default']) then
       local expression = rspamd_expression.create(meta_rule.expression,
-          parse_sa_atom,
-          rspamd_config:get_mempool())
+        parse_sa_atom,
+        rspamd_config:get_mempool())
       if not expression then
         rspamd_logger.errx(rspamd_config, 'Cannot parse SA meta expression: %s', meta_rule.expression)
         return
@@ -544,11 +624,11 @@ create_sa_meta_callback = function(meta_rule)
 
         if res > 0 then
           local filtered_trace = fun.totable(fun.take_n(5,
-              fun.map(function(elt)
-                return elt:gsub('^__', '')
-              end, fun.filter(exclude_sym_filter, trace))))
+            fun.map(function(elt)
+              return elt:gsub('^__', '')
+            end, fun.filter(exclude_sym_filter, trace))))
           lua_util.debugm(N, task, 'SA meta %s matched with result: %s; trace %s; filtered trace %s',
-              meta_rule.symbol, res, trace, filtered_trace)
+            meta_rule.symbol, res, trace, filtered_trace)
           task:insert_result_named(cur_res, meta_rule.symbol, 1.0, filtered_trace)
         end
 
@@ -576,14 +656,14 @@ end
 -- Initialize SA meta rules after all atoms are processed
 local function finalize_sa_rules()
   lua_util.debugm(N, rspamd_config, 'Finalizing SA rules - processing %s meta rules',
-      fun.length(sa_meta_rules))
+    fun.length(sa_meta_rules))
 
   for meta_name, meta_rule in pairs(sa_meta_rules) do
     local score = sa_scores[meta_name] or 1.0
     local description = sa_descriptions[meta_name] or ('multimap symbol ' .. meta_name)
 
     lua_util.debugm(N, rspamd_config, 'Registering SA meta rule %s (score: %s, expression: %s)',
-        meta_name, score, meta_rule.expression)
+      meta_name, score, meta_rule.expression)
 
     local id = rspamd_config:register_symbol({
       name = meta_name,
@@ -595,7 +675,7 @@ local function finalize_sa_rules()
     })
 
     lua_util.debugm(N, rspamd_config, 'Successfully registered SA meta symbol %s with id %s (callback attached)',
-        meta_name, id)
+      meta_name, id)
 
     rspamd_config:set_metric_symbol({
       name = meta_name,
@@ -619,7 +699,7 @@ local function finalize_sa_rules()
     end
 
     lua_util.debugm(N, rspamd_config, 'registered SA meta symbol: %s (score: %s)',
-        meta_name, score)
+      meta_name, score)
   end
 
   -- Mark orphaned symbols - only check meta symbols (not atoms) since atoms are just expression parts
@@ -632,7 +712,7 @@ local function finalize_sa_rules()
   end
 
   lua_util.debugm(N, rspamd_config, 'SA rules finalization complete: registered %s meta rules with callbacks',
-      fun.length(sa_meta_rules))
+    fun.length(sa_meta_rules))
 end
 
 -- Helper function to get regexp_rules symbol state statistics (only meta symbols, not atoms)
@@ -674,7 +754,7 @@ local function sync_regexp_rules_symbol_states()
         end
 
         lua_util.debugm(N, rspamd_config, 'Scope %s is loaded, marked %s symbols as available',
-            rule.scope_name, updated_count)
+          rule.scope_name, updated_count)
       else
         lua_util.debugm(N, rspamd_config, 'Scope %s is not loaded', rule.scope_name)
       end
@@ -683,7 +763,7 @@ local function sync_regexp_rules_symbol_states()
 
   local stats = get_regexp_rules_symbol_stats()
   lua_util.debugm(N, rspamd_config, 'Symbol state stats after sync: available=%s, loading=%s, orphaned=%s, total=%s',
-      stats.available, stats.loading, stats.orphaned, stats.total)
+    stats.available, stats.loading, stats.orphaned, stats.total)
 end
 
 -- Optional cleanup function to remove old orphaned symbols (can be called periodically)
@@ -1130,19 +1210,19 @@ local function multimap_query_redis(key, task, value, callback)
 
   local function redis_map_cb(err, data)
     lua_util.debugm(N, task, 'got reply from Redis when trying to get key %s: err=%s, data=%s',
-        key, err, data)
+      key, err, data)
     if not err and type(data) ~= 'userdata' then
       callback(data)
     end
   end
 
   return rspamd_redis_make_request(task,
-      redis_params, -- connect params
-      key, -- hash key
-      false, -- is write
-      redis_map_cb, --callback
-      cmd, -- command
-      srch          -- arguments
+    redis_params, -- connect params
+    key,          -- hash key
+    false,        -- is write
+    redis_map_cb, --callback
+    cmd,          -- command
+    srch          -- arguments
   )
 end
 
@@ -1154,9 +1234,9 @@ local function multimap_callback(task, rule)
 
     local function get_key_callback(ret, err_or_data, err_code)
       lua_util.debugm(N, task, 'got return "%s" (err code = %s) for multimap %s',
-          err_or_data,
-          err_code,
-          rule.symbol)
+        err_or_data,
+        err_code,
+        rule.symbol)
 
       if ret then
         if type(err_or_data) == 'table' then
@@ -1168,12 +1248,12 @@ local function multimap_callback(task, rule)
         end
       elseif err_code ~= 404 then
         rspamd_logger.infox(task, "map %s: get key returned error %s: %s",
-            rule.symbol, err_code, err_or_data)
+          rule.symbol, err_code, err_or_data)
       end
     end
 
     lua_util.debugm(N, task, 'check value %s for multimap %s', value,
-        rule.symbol)
+      rule.symbol)
 
     local ret = false
 
@@ -1212,8 +1292,8 @@ local function multimap_callback(task, rule)
       if rule.symbols_set then
         if not rule.symbols_set[symbol] then
           rspamd_logger.infox(task, 'symbol %s is not registered for map %s, ' ..
-              'replace it with just %s',
-              symbol, rule.symbol, rule.symbol)
+            'replace it with just %s',
+            symbol, rule.symbol, rule.symbol)
           symbol = rule.symbol
         end
       elseif rule.disable_multisymbol then
@@ -1283,7 +1363,7 @@ local function multimap_callback(task, rule)
       if fn then
         local filtered_value = fn(task, r.filter, value, r)
         lua_util.debugm(N, task, 'apply filter %s for rule %s: %s -> %s',
-            r.filter, r.symbol, value, filtered_value)
+          r.filter, r.symbol, value, filtered_value)
         value = filtered_value
       end
     end
@@ -1430,12 +1510,12 @@ local function multimap_callback(task, rule)
 
     if not res or res == 0 then
       lua_util.debugm(N, task, 'condition is false for %s',
-          rule.symbol)
+        rule.symbol)
       return
     else
       lua_util.debugm(N, task, 'condition is true for %s: %s',
-          rule.symbol,
-          trace)
+        rule.symbol,
+        trace)
     end
   end
 
@@ -1452,18 +1532,18 @@ local function multimap_callback(task, rule)
         local to_resolve = ip_to_rbl(ip, rule['map'])
         local function dns_cb(_, _, results, err)
           lua_util.debugm(N, rspamd_config,
-              'resolve() finished: results=%1, err=%2, to_resolve=%3',
-              results, err, to_resolve)
+            'resolve() finished: results=%1, err=%2, to_resolve=%3',
+            results, err, to_resolve)
 
           if err and
               (err ~= 'requested record is not found' and
-                  err ~= 'no records with this name') then
+                err ~= 'no records with this name') then
             rspamd_logger.errx(task, 'error looking up %s: %s', to_resolve, results)
           elseif results then
             task:insert_result(rule['symbol'], 1, rule['map'])
             if rule.action then
               task:set_pre_result(rule['action'],
-                  'Matched map: ' .. rule['symbol'], N)
+                'Matched map: ' .. rule['symbol'], N)
             end
           end
         end
@@ -1629,7 +1709,7 @@ local function multimap_callback(task, rule)
           if ext then
             local fake_fname = string.format('detected.%s', ext)
             lua_util.debugm(N, task, 'detected filename %s',
-                fake_fname)
+              fake_fname)
             match_filename(rule, fake_fname)
           end
         end
@@ -1702,7 +1782,7 @@ local function multimap_callback(task, rule)
       if ret and ret ~= 0 then
         for n, t in pairs(trace) do
           insert_results(t.value, string.format("%s=%s",
-              n, t.matched))
+            n, t.matched))
         end
       end
     end,
@@ -1710,7 +1790,7 @@ local function multimap_callback(task, rule)
       -- For regexp_rules, the meta rules are registered as separate symbols
       -- This is just a placeholder callback
       lua_util.debugm(N, task, 'Regexp rules callback for %s - meta rules are registered as separate symbols',
-          rule.symbol)
+        rule.symbol)
     end,
   }
 
@@ -1739,7 +1819,7 @@ local function multimap_on_load_gen(rule)
 
       if r and symbol and not known_symbols[symbol] then
         lua_util.debugm(N, rspamd_config, "%s: adding new symbol %s (score = %s), triggered by %s",
-            rule.symbol, symbol, score, key)
+          rule.symbol, symbol, score, key)
         rspamd_config:register_symbol {
           name = symbol,
           parent = rule.callback_id,
@@ -1765,22 +1845,22 @@ local function add_multimap_rule(key, newrule)
     if rule['regexp'] then
       if rule['multi'] then
         rule.map_obj = lua_maps.map_add_from_ucl(rule.map, 'regexp_multi',
-            rule.description)
+          rule.description)
       else
         rule.map_obj = lua_maps.map_add_from_ucl(rule.map, 'regexp',
-            rule.description)
+          rule.description)
       end
     elseif rule['glob'] then
       if rule['multi'] then
         rule.map_obj = lua_maps.map_add_from_ucl(rule.map, 'glob_multi',
-            rule.description)
+          rule.description)
       else
         rule.map_obj = lua_maps.map_add_from_ucl(rule.map, 'glob',
-            rule.description)
+          rule.description)
       end
     else
       rule.map_obj = lua_maps.map_add_from_ucl(rule.map, 'hash',
-          rule.description)
+        rule.description)
     end
   end
 
@@ -1821,7 +1901,7 @@ local function add_multimap_rule(key, newrule)
   end
   if not newrule['description'] then
     newrule['description'] = string.format('multimap, type %s: %s', newrule['type'],
-        newrule['symbol'])
+      newrule['symbol'])
   end
   if newrule['type'] == 'mempool' and not newrule['variable'] then
     rspamd_logger.errx(rspamd_config, 'mempool map requires variable')
@@ -1833,11 +1913,11 @@ local function add_multimap_rule(key, newrule)
       return nil
     else
       local selector = lua_selectors.create_selector_closure(
-          rspamd_config, newrule['selector'], newrule['delimiter'] or "")
+        rspamd_config, newrule['selector'], newrule['delimiter'] or "")
 
       if not selector then
         rspamd_logger.errx(rspamd_config, 'selector map has invalid selector: "%s", symbol: %s',
-            newrule['selector'], newrule['symbol'])
+          newrule['selector'], newrule['symbol'])
         return nil
       end
 
@@ -1848,7 +1928,7 @@ local function add_multimap_rule(key, newrule)
       string.find(newrule['map'], '^redis://.*$') then
     if not redis_params then
       rspamd_logger.infox(rspamd_config, 'no redis servers are specified, ' ..
-          'cannot add redis map %s: %s', newrule['symbol'], newrule['map'])
+        'cannot add redis map %s: %s', newrule['symbol'], newrule['map'])
       return nil
     end
 
@@ -1861,17 +1941,17 @@ local function add_multimap_rule(key, newrule)
       string.find(newrule['map'], '^redis%+selector://.*$') then
     if not redis_params then
       rspamd_logger.infox(rspamd_config, 'no redis servers are specified, ' ..
-          'cannot add redis map %s: %s', newrule['symbol'], newrule['map'])
+        'cannot add redis map %s: %s', newrule['symbol'], newrule['map'])
       return nil
     end
 
     local selector_str = string.match(newrule['map'], '^redis%+selector://(.*)$')
     local selector = lua_selectors.create_selector_closure(
-        rspamd_config, selector_str, newrule['delimiter'] or "")
+      rspamd_config, selector_str, newrule['delimiter'] or "")
 
     if not selector then
       rspamd_logger.errx(rspamd_config, 'redis selector map has invalid selector: "%s", symbol: %s',
-          selector_str, newrule['symbol'])
+        selector_str, newrule['symbol'])
       return nil
     end
 
@@ -1880,12 +1960,12 @@ local function add_multimap_rule(key, newrule)
   elseif newrule.type == 'combined' then
     local lua_maps_expressions = require "lua_maps_expressions"
     newrule.combined = lua_maps_expressions.create(rspamd_config,
-        {
-          rules = newrule.rules,
-          expression = newrule.expression,
-          description = newrule.description,
-          on_load = newrule.dynamic_symbols and multimap_on_load_gen(newrule) or nil,
-        }, N, 'Combined map for ' .. newrule.symbol)
+      {
+        rules = newrule.rules,
+        expression = newrule.expression,
+        description = newrule.description,
+        on_load = newrule.dynamic_symbols and multimap_on_load_gen(newrule) or nil,
+      }, N, 'Combined map for ' .. newrule.symbol)
     if not newrule.combined then
       rspamd_logger.errx(rspamd_config, 'cannot add combined map for %s', newrule.symbol)
     else
@@ -1940,7 +2020,7 @@ local function add_multimap_rule(key, newrule)
             if state_info.rule_name == newrule.symbol then
               state_info.state = 'loading'
               lua_util.debugm(N, rspamd_config, 'marked regexp_rules symbol %s as loading for scope %s reload',
-                  symbol, scope_name)
+                symbol, scope_name)
             end
           end
 
@@ -1956,7 +2036,7 @@ local function add_multimap_rule(key, newrule)
             sa_atoms[symbol] = nil
             sa_meta_rules[symbol] = nil
             lua_util.debugm(N, rspamd_config, 'cleared regexp_rules symbol %s for scope %s reload',
-                symbol, scope_name)
+              symbol, scope_name)
           end
 
           -- The scope will be created by process_sa_line when first regexp is added
@@ -1966,7 +2046,7 @@ local function add_multimap_rule(key, newrule)
         end
         process_sa_line(newrule, line)
       end,
-      by_line = true, -- Process line by line
+      by_line = true,      -- Process line by line
       opaque_data = false, -- Use plain strings
     })
 
@@ -2002,7 +2082,7 @@ local function add_multimap_rule(key, newrule)
           -- Trigger hyperscan compilation for this updated scope
           newrule.map_obj:trigger_hyperscan_compilation()
           lua_util.debugm(N, rspamd_config, 'triggered hyperscan compilation for scope %s after map loading',
-              scope_name)
+            scope_name)
         else
           lua_util.debugm(N, rspamd_config, 'regexp scope %s not created (empty map)', scope_name)
         end
@@ -2016,7 +2096,7 @@ local function add_multimap_rule(key, newrule)
         -- Promote symcache resort after dynamic symbol registration
         rspamd_config:promote_symbols_cache_resort()
         lua_util.debugm(N, rspamd_config, 'promoted symcache resort after loading SA rules from map %s',
-            newrule.symbol)
+          newrule.symbol)
       end)
     end
 
@@ -2024,21 +2104,21 @@ local function add_multimap_rule(key, newrule)
       -- Mark this rule as using SA functionality
       newrule.uses_sa = true
       lua_util.debugm(N, rspamd_config, 'created regexp_rules map %s with scope: %s',
-          newrule.symbol, scope_name)
+        newrule.symbol, scope_name)
       ret = true
     else
       rspamd_logger.warnx(rspamd_config, 'Cannot add SA-style rule: map doesn\'t exists: %s',
-          newrule['map'])
+        newrule['map'])
     end
   else
     if newrule['type'] == 'ip' then
       newrule.map_obj = lua_maps.map_add_from_ucl(newrule.map, 'radix',
-          newrule.description)
+        newrule.description)
       if newrule.map_obj then
         ret = true
       else
         rspamd_logger.warnx(rspamd_config, 'Cannot add rule: map doesn\'t exists: %s',
-            newrule['map'])
+          newrule['map'])
       end
     elseif newrule['type'] == 'received' then
       if type(newrule['flags']) == 'table' and newrule['flags'][1] then
@@ -2054,12 +2134,12 @@ local function add_multimap_rule(key, newrule)
       local filter = newrule['filter'] or 'real_ip'
       if filter == 'real_ip' or filter == 'from_ip' then
         newrule.map_obj = lua_maps.map_add_from_ucl(newrule.map, 'radix',
-            newrule.description)
+          newrule.description)
         if newrule.map_obj then
           ret = true
         else
           rspamd_logger.warnx(rspamd_config, 'Cannot add rule: map doesn\'t exists: %s',
-              newrule['map'])
+            newrule['map'])
         end
       else
         multimap_load_kv_map(newrule)
@@ -2068,13 +2148,13 @@ local function add_multimap_rule(key, newrule)
           ret = true
         else
           rspamd_logger.warnx(rspamd_config, 'Cannot add rule: map doesn\'t exists: %s',
-              newrule['map'])
+            newrule['map'])
         end
       end
     elseif known_generic_types[newrule.type] then
       if newrule.filter == 'ip_addr' then
         newrule.map_obj = lua_maps.map_add_from_ucl(newrule.map, 'radix',
-            newrule.description)
+          newrule.description)
       elseif not newrule.combined then
         multimap_load_kv_map(newrule)
       end
@@ -2083,13 +2163,13 @@ local function add_multimap_rule(key, newrule)
         ret = true
       else
         rspamd_logger.warnx(rspamd_config, 'Cannot add rule: map doesn\'t exists: %s',
-            newrule['map'])
+          newrule['map'])
       end
     elseif newrule['type'] == 'dnsbl' then
       ret = true
     else
       rspamd_logger.errx(rspamd_config, 'cannot add rule %s: invalid type %s',
-          key, newrule['type'])
+        key, newrule['type'])
     end
   end
 
@@ -2126,13 +2206,13 @@ local function add_multimap_rule(key, newrule)
       end
 
       local expression = rspamd_expression.create(newrule['require_symbols'],
-          { parse_atom, process_atom }, rspamd_config:get_mempool())
+        { parse_atom, process_atom }, rspamd_config:get_mempool())
       if expression then
         newrule['expression'] = expression
 
         fun.each(function(v)
           lua_util.debugm(N, rspamd_config, 'add dependency %s -> %s',
-              newrule['symbol'], v)
+            newrule['symbol'], v)
           rspamd_config:register_dependency(newrule['symbol'], v)
         end, atoms)
       end
@@ -2177,7 +2257,7 @@ if opts and type(opts) == 'table' then
         rspamd_logger.errx(rspamd_config, 'cannot add rule: "' .. k .. '"')
       else
         rspamd_logger.infox(rspamd_config, 'added multimap rule: %s (%s)',
-            k, rule.type)
+          k, rule.type)
         table.insert(rules, rule)
       end
     end