From: Vsevolod Stakhov Date: Thu, 27 Nov 2025 15:37:47 +0000 (+0000) Subject: [Feature] Add combinator option for multimap selector rules X-Git-Tag: 3.14.1~4^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=refs%2Fpull%2F5766%2Fhead;p=thirdparty%2Frspamd.git [Feature] Add combinator option for multimap selector rules This change adds support for structured data output from selectors in multimap rules. Previously, selectors always produced concatenated strings which made it impossible to send structured JSON data to external map services. New 'combinator' option for selector-type multimap rules: - 'string' (default): concatenate results with delimiter (existing behavior) - 'array': flatten all results into a flat array - 'object': convert pairs of selectors into key-value object Example configuration for external JSON API: multimap { MY_EXTERNAL_CHECK { type = "selector"; selector = "id('from');from('smtp'):addr;id('ip');ip"; combinator = "object"; # produces {"from": "...", "ip": "..."} map = { external = true; backend = "http://api.example.com/check"; method = "body"; encode = "json"; }; } } Changes: - lua_selectors: Added combinator registry and helper functions - get_combinator(name): returns combinator function by name - list_combinators(): returns available combinator names - create_selector_closure_with_combinator(): creates closure with named combinator - multimap: Added 'combinator' option support for selector and redis+selector maps --- diff --git a/lualib/lua_selectors/init.lua b/lualib/lua_selectors/init.lua index 5fcdb38338..bfb012a692 100644 --- a/lualib/lua_selectors/init.lua +++ b/lualib/lua_selectors/init.lua @@ -662,6 +662,61 @@ exports.add_map = function(name, map) end end +-- Combinator functions registry for use with external maps and other consumers +-- that need structured data instead of plain strings +local combinators = { + -- Default: concatenate all selector results into a single string + string = exports.combine_selectors, + -- Flatten all results into a flat array + array = exports.flatten_selectors, + -- Convert pairs of selectors into key-value object (use `id` extractor for keys) + object = exports.kv_table_from_pairs, +} + +--[[[ +-- @function lua_selectors.get_combinator(name) +-- Returns a combinator function by name +-- @param {string} name combinator name: 'string', 'array', or 'object' +-- @return {function} combinator function or nil if not found +--]] +exports.get_combinator = function(name) + return combinators[name] +end + +--[[[ +-- @function lua_selectors.list_combinators() +-- Returns list of available combinator names +-- @return {table} array of combinator names +--]] +exports.list_combinators = function() + local res = {} + for k, _ in pairs(combinators) do + table.insert(res, k) + end + return res +end + +--[[[ +-- @function lua_selectors.create_selector_closure_with_combinator(cfg, selector_str, delimiter, combinator_name) +-- Creates a closure from a string selector using named combinator +-- @param {rspamd_config} cfg rspamd config object +-- @param {string} selector_str selector string to parse +-- @param {string} delimiter delimiter for combining results (used by some combinators) +-- @param {string} combinator_name name of combinator: 'string', 'array', or 'object' +-- @return {function} closure that processes selector on task, or nil on error +--]] +exports.create_selector_closure_with_combinator = function(cfg, selector_str, delimiter, combinator_name) + local combinator_fn = combinators[combinator_name or 'string'] + + if not combinator_fn then + logger.errx(cfg, 'unknown combinator: %s, available: %s', + combinator_name, table.concat(exports.list_combinators(), ', ')) + return nil + end + + return exports.create_selector_closure_fn(cfg, cfg, selector_str, delimiter, combinator_fn) +end + -- Publish log target exports.M = M diff --git a/src/plugins/lua/multimap.lua b/src/plugins/lua/multimap.lua index 1b186ef7f9..4510d00178 100644 --- a/src/plugins/lua/multimap.lua +++ b/src/plugins/lua/multimap.lua @@ -2010,13 +2010,32 @@ local function add_multimap_rule(key, newrule) rspamd_logger.errx(rspamd_config, 'selector map requires selector definition') return nil else - local selector = lua_selectors.create_selector_closure( - rspamd_config, newrule['selector'], newrule['delimiter'] or "") + local selector + local combinator = newrule['combinator'] + + if combinator then + -- Use named combinator for structured output (array, object, string) + selector = lua_selectors.create_selector_closure_with_combinator( + rspamd_config, newrule['selector'], newrule['delimiter'] or "", combinator) + + if not selector then + rspamd_logger.errx(rspamd_config, 'selector map has invalid selector or combinator: "%s", combinator: %s, symbol: %s', + newrule['selector'], combinator, newrule['symbol']) + return nil + end - if not selector then - rspamd_logger.errx(rspamd_config, 'selector map has invalid selector: "%s", symbol: %s', - newrule['selector'], newrule['symbol']) - return nil + lua_util.debugm(N, rspamd_config, 'created selector with combinator %s for rule %s', + combinator, newrule['symbol']) + else + -- Default behavior: use combine_selectors (string output) + selector = lua_selectors.create_selector_closure( + 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']) + return nil + end end newrule.selector = selector @@ -2044,13 +2063,30 @@ local function add_multimap_rule(key, newrule) end local selector_str = string.match(newrule['map'], '^redis%+selector://(.*)$') - local selector = lua_selectors.create_selector_closure( - rspamd_config, selector_str, newrule['delimiter'] or "") + local selector + local combinator = newrule['combinator'] - if not selector then - rspamd_logger.errx(rspamd_config, 'redis selector map has invalid selector: "%s", symbol: %s', - selector_str, newrule['symbol']) - return nil + if combinator then + selector = lua_selectors.create_selector_closure_with_combinator( + rspamd_config, selector_str, newrule['delimiter'] or "", combinator) + + if not selector then + rspamd_logger.errx(rspamd_config, 'redis selector map has invalid selector or combinator: "%s", combinator: %s, symbol: %s', + selector_str, combinator, newrule['symbol']) + return nil + end + + lua_util.debugm(N, rspamd_config, 'created redis selector with combinator %s for rule %s', + combinator, newrule['symbol']) + else + selector = lua_selectors.create_selector_closure( + 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']) + return nil + end end newrule['redis_key'] = selector