You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

multimap.lua 40KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463
  1. --[[
  2. Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. ]]--
  13. if confighelp then
  14. return
  15. end
  16. -- Multimap is rspamd module designed to define and operate with different maps
  17. local rules = {}
  18. local rspamd_logger = require "rspamd_logger"
  19. local rspamd_util = require "rspamd_util"
  20. local rspamd_regexp = require "rspamd_regexp"
  21. local rspamd_expression = require "rspamd_expression"
  22. local rspamd_ip = require "rspamd_ip"
  23. local lua_util = require "lua_util"
  24. local lua_selectors = require "lua_selectors"
  25. local lua_maps = require "lua_maps"
  26. local redis_params
  27. local fun = require "fun"
  28. local N = 'multimap'
  29. local multimap_grammar
  30. -- Parse result in form: <symbol>:<score>|<symbol>|<score>
  31. local function parse_multimap_value(parse_rule, p_ret)
  32. if p_ret and type(p_ret) == 'string' then
  33. local lpeg = require "lpeg"
  34. if not multimap_grammar then
  35. local number = {}
  36. local digit = lpeg.R("09")
  37. number.integer = (lpeg.S("+-") ^ -1) *
  38. (digit ^ 1)
  39. -- Matches: .6, .899, .9999873
  40. number.fractional = (lpeg.P(".")) *
  41. (digit ^ 1)
  42. -- Matches: 55.97, -90.8, .9
  43. number.decimal = (number.integer * -- Integer
  44. (number.fractional ^ -1)) + -- Fractional
  45. (lpeg.S("+-") * number.fractional) -- Completely fractional number
  46. local sym_start = lpeg.R("az", "AZ") + lpeg.S("_")
  47. local sym_elt = sym_start + lpeg.R("09")
  48. local symbol = sym_start * sym_elt ^ 0
  49. local symbol_cap = lpeg.Cg(symbol, 'symbol')
  50. local score_cap = lpeg.Cg(number.decimal, 'score')
  51. local opts_cap = lpeg.Cg(lpeg.Ct(lpeg.C(symbol) * (lpeg.P(",") * lpeg.C(symbol)) ^ 0), 'opts')
  52. local symscore_cap = (symbol_cap * lpeg.P(":") * score_cap)
  53. local symscoreopt_cap = symscore_cap * lpeg.P(":") * opts_cap
  54. local grammar = symscoreopt_cap + symscore_cap + symbol_cap + score_cap
  55. multimap_grammar = lpeg.Ct(grammar)
  56. end
  57. local tbl = multimap_grammar:match(p_ret)
  58. if tbl then
  59. local sym
  60. local score = 1.0
  61. local opts = {}
  62. if tbl.symbol then
  63. sym = tbl.symbol
  64. end
  65. if tbl.score then
  66. score = tonumber(tbl.score)
  67. end
  68. if tbl.opts then
  69. opts = tbl.opts
  70. end
  71. return true, sym, score, opts
  72. else
  73. if p_ret ~= '' then
  74. rspamd_logger.infox(rspamd_config, '%s: cannot parse string "%s"',
  75. parse_rule.symbol, p_ret)
  76. end
  77. return true, nil, 1.0, {}
  78. end
  79. elseif type(p_ret) == 'boolean' then
  80. return p_ret, nil, 1.0, {}
  81. end
  82. return false, nil, 0.0, {}
  83. end
  84. local value_types = {
  85. ip = {
  86. get_value = function(ip)
  87. return ip:to_string()
  88. end,
  89. },
  90. from = {
  91. get_value = function(val)
  92. return val
  93. end,
  94. },
  95. helo = {
  96. get_value = function(val)
  97. return val
  98. end,
  99. },
  100. header = {
  101. get_value = function(val)
  102. return val
  103. end,
  104. },
  105. rcpt = {
  106. get_value = function(val)
  107. return val
  108. end,
  109. },
  110. user = {
  111. get_value = function(val)
  112. return val
  113. end,
  114. },
  115. url = {
  116. get_value = function(val)
  117. return val
  118. end,
  119. },
  120. dnsbl = {
  121. get_value = function(ip)
  122. return ip:to_string()
  123. end,
  124. },
  125. filename = {
  126. get_value = function(val)
  127. return val
  128. end,
  129. },
  130. content = {
  131. get_value = function()
  132. return nil
  133. end,
  134. },
  135. hostname = {
  136. get_value = function(val)
  137. return val
  138. end,
  139. },
  140. asn = {
  141. get_value = function(val)
  142. return val
  143. end,
  144. },
  145. country = {
  146. get_value = function(val)
  147. return val
  148. end,
  149. },
  150. received = {
  151. get_value = function(val)
  152. return val
  153. end,
  154. },
  155. mempool = {
  156. get_value = function(val)
  157. return val
  158. end,
  159. },
  160. selector = {
  161. get_value = function(val)
  162. return val
  163. end,
  164. },
  165. symbol_options = {
  166. get_value = function(val)
  167. return val
  168. end,
  169. },
  170. }
  171. local function ip_to_rbl(ip, rbl)
  172. return table.concat(ip:inversed_str_octets(), ".") .. '.' .. rbl
  173. end
  174. local function apply_hostname_filter(task, filter, hostname, r)
  175. if filter == 'tld' then
  176. local tld = rspamd_util.get_tld(hostname)
  177. return tld
  178. elseif filter == 'top' then
  179. local tld = rspamd_util.get_tld(hostname)
  180. return tld:match('[^.]*$') or tld
  181. else
  182. if not r['re_filter'] then
  183. local pat = string.match(filter, 'tld:regexp:(.+)')
  184. if not pat then
  185. rspamd_logger.errx(task, 'bad search filter: %s', filter)
  186. return
  187. end
  188. r['re_filter'] = rspamd_regexp.create_cached(pat)
  189. if not r['re_filter'] then
  190. rspamd_logger.errx(task, 'couldnt create regex: %s', pat)
  191. return
  192. end
  193. end
  194. local tld = rspamd_util.get_tld(hostname)
  195. local res = r['re_filter']:search(tld)
  196. if res then
  197. return res[1]
  198. else
  199. return nil
  200. end
  201. end
  202. end
  203. local function apply_url_filter(task, filter, url, r)
  204. if not filter then
  205. return url:get_host()
  206. end
  207. if filter == 'tld' then
  208. return url:get_tld()
  209. elseif filter == 'top' then
  210. local tld = url:get_tld()
  211. return tld:match('[^.]*$') or tld
  212. elseif filter == 'full' then
  213. return url:get_text()
  214. elseif filter == 'is_phished' then
  215. if url:is_phished() then
  216. return url:get_host()
  217. else
  218. return nil
  219. end
  220. elseif filter == 'is_redirected' then
  221. if url:is_redirected() then
  222. return url:get_host()
  223. else
  224. return nil
  225. end
  226. elseif filter == 'is_obscured' then
  227. if url:is_obscured() then
  228. return url:get_host()
  229. else
  230. return nil
  231. end
  232. elseif filter == 'path' then
  233. return url:get_path()
  234. elseif filter == 'query' then
  235. return url:get_query()
  236. elseif string.find(filter, 'tag:') then
  237. local tags = url:get_tags()
  238. local want_tag = string.match(filter, 'tag:(.*)')
  239. for _, t in ipairs(tags) do
  240. if t == want_tag then
  241. return url:get_host()
  242. end
  243. end
  244. return nil
  245. elseif string.find(filter, 'tld:regexp:') then
  246. if not r['re_filter'] then
  247. local type, pat = string.match(filter, '(regexp:)(.+)')
  248. if type and pat then
  249. r['re_filter'] = rspamd_regexp.create_cached(pat)
  250. end
  251. end
  252. if not r['re_filter'] then
  253. rspamd_logger.errx(task, 'bad search filter: %s', filter)
  254. else
  255. local results = r['re_filter']:search(url:get_tld())
  256. if results then
  257. return results[1]
  258. else
  259. return nil
  260. end
  261. end
  262. elseif string.find(filter, 'full:regexp:') then
  263. if not r['re_filter'] then
  264. local type, pat = string.match(filter, '(regexp:)(.+)')
  265. if type and pat then
  266. r['re_filter'] = rspamd_regexp.create_cached(pat)
  267. end
  268. end
  269. if not r['re_filter'] then
  270. rspamd_logger.errx(task, 'bad search filter: %s', filter)
  271. else
  272. local results = r['re_filter']:search(url:get_text())
  273. if results then
  274. return results[1]
  275. else
  276. return nil
  277. end
  278. end
  279. elseif string.find(filter, 'regexp:') then
  280. if not r['re_filter'] then
  281. local type, pat = string.match(filter, '(regexp:)(.+)')
  282. if type and pat then
  283. r['re_filter'] = rspamd_regexp.create_cached(pat)
  284. end
  285. end
  286. if not r['re_filter'] then
  287. rspamd_logger.errx(task, 'bad search filter: %s', filter)
  288. else
  289. local results = r['re_filter']:search(url:get_host())
  290. if results then
  291. return results[1]
  292. else
  293. return nil
  294. end
  295. end
  296. elseif string.find(filter, '^template:') then
  297. if not r['template'] then
  298. r['template'] = string.match(filter, '^template:(.+)')
  299. end
  300. if r['template'] then
  301. return lua_util.template(r['template'], url:to_table())
  302. end
  303. end
  304. return url:get_host()
  305. end
  306. local function apply_addr_filter(task, filter, input, rule)
  307. if filter == 'email:addr' or filter == 'email' then
  308. local addr = rspamd_util.parse_mail_address(input, task:get_mempool(), 1024)
  309. if addr and addr[1] then
  310. return fun.totable(fun.map(function(a)
  311. return a.addr
  312. end, addr))
  313. end
  314. elseif filter == 'email:user' then
  315. local addr = rspamd_util.parse_mail_address(input, task:get_mempool(), 1024)
  316. if addr and addr[1] then
  317. return fun.totable(fun.map(function(a)
  318. return a.user
  319. end, addr))
  320. end
  321. elseif filter == 'email:domain' then
  322. local addr = rspamd_util.parse_mail_address(input, task:get_mempool(), 1024)
  323. if addr and addr[1] then
  324. return fun.totable(fun.map(function(a)
  325. return a.domain
  326. end, addr))
  327. end
  328. elseif filter == 'email:domain:tld' then
  329. local addr = rspamd_util.parse_mail_address(input, task:get_mempool(), 1024)
  330. if addr and addr[1] then
  331. return fun.totable(fun.map(function(a)
  332. return rspamd_util.get_tld(a.domain)
  333. end, addr))
  334. end
  335. elseif filter == 'email:name' then
  336. local addr = rspamd_util.parse_mail_address(input, task:get_mempool(), 1024)
  337. if addr and addr[1] then
  338. return fun.totable(fun.map(function(a)
  339. return a.name
  340. end, addr))
  341. end
  342. elseif filter == 'ip_addr' then
  343. local ip_addr = rspamd_ip.from_string(input)
  344. if ip_addr and ip_addr:is_valid() then
  345. return ip_addr
  346. end
  347. else
  348. -- regexp case
  349. if not rule['re_filter'] then
  350. local type, pat = string.match(filter, '(regexp:)(.+)')
  351. if type and pat then
  352. rule['re_filter'] = rspamd_regexp.create_cached(pat)
  353. end
  354. end
  355. if not rule['re_filter'] then
  356. rspamd_logger.errx(task, 'bad search filter: %s', filter)
  357. else
  358. local results = rule['re_filter']:search(input)
  359. if results then
  360. return results[1]
  361. end
  362. end
  363. end
  364. return input
  365. end
  366. local function apply_filename_filter(task, filter, fn, r)
  367. if filter == 'extension' or filter == 'ext' then
  368. return string.match(fn, '%.([^.]+)$')
  369. elseif string.find(filter, 'regexp:') then
  370. if not r['re_filter'] then
  371. local type, pat = string.match(filter, '(regexp:)(.+)')
  372. if type and pat then
  373. r['re_filter'] = rspamd_regexp.create_cached(pat)
  374. end
  375. end
  376. if not r['re_filter'] then
  377. rspamd_logger.errx(task, 'bad search filter: %s', filter)
  378. else
  379. local results = r['re_filter']:search(fn)
  380. if results then
  381. return results[1]
  382. else
  383. return nil
  384. end
  385. end
  386. end
  387. return fn
  388. end
  389. local function apply_regexp_filter(task, filter, fn, r)
  390. if string.find(filter, 'regexp:') then
  391. if not r['re_filter'] then
  392. local type, pat = string.match(filter, '(regexp:)(.+)')
  393. if type and pat then
  394. r['re_filter'] = rspamd_regexp.create_cached(pat)
  395. end
  396. end
  397. if not r['re_filter'] then
  398. rspamd_logger.errx(task, 'bad search filter: %s', filter)
  399. else
  400. local results = r['re_filter']:search(fn, false, true)
  401. if results then
  402. return results[1][2]
  403. else
  404. return nil
  405. end
  406. end
  407. end
  408. return fn
  409. end
  410. local function apply_content_filter(task, filter)
  411. if filter == 'body' then
  412. return { task:get_rawbody() }
  413. elseif filter == 'full' then
  414. return { task:get_content() }
  415. elseif filter == 'headers' then
  416. return { task:get_raw_headers() }
  417. elseif filter == 'text' then
  418. local ret = {}
  419. for _, p in ipairs(task:get_text_parts()) do
  420. table.insert(ret, p:get_content())
  421. end
  422. return ret
  423. elseif filter == 'rawtext' then
  424. local ret = {}
  425. for _, p in ipairs(task:get_text_parts()) do
  426. table.insert(ret, p:get_content('raw_parsed'))
  427. end
  428. return ret
  429. elseif filter == 'oneline' then
  430. local ret = {}
  431. for _, p in ipairs(task:get_text_parts()) do
  432. table.insert(ret, p:get_content_oneline())
  433. end
  434. return ret
  435. else
  436. rspamd_logger.errx(task, 'bad search filter: %s', filter)
  437. end
  438. return {}
  439. end
  440. local multimap_filters = {
  441. from = apply_addr_filter,
  442. rcpt = apply_addr_filter,
  443. helo = apply_hostname_filter,
  444. symbol_options = apply_regexp_filter,
  445. header = apply_addr_filter,
  446. url = apply_url_filter,
  447. filename = apply_filename_filter,
  448. mempool = apply_regexp_filter,
  449. selector = apply_regexp_filter,
  450. hostname = apply_hostname_filter,
  451. --content = apply_content_filter, -- Content filters are special :(
  452. }
  453. local function multimap_query_redis(key, task, value, callback)
  454. local cmd = 'HGET'
  455. if type(value) == 'userdata' and value.class == 'rspamd{ip}' then
  456. cmd = 'HMGET'
  457. end
  458. local srch = { key }
  459. -- Insert all ips for some mask :(
  460. if type(value) == 'userdata' and value.class == 'rspamd{ip}' then
  461. srch[#srch + 1] = tostring(value)
  462. -- IPv6 case
  463. local maxbits = 128
  464. local minbits = 64
  465. if value:get_version() == 4 then
  466. maxbits = 32
  467. minbits = 8
  468. end
  469. for i = maxbits, minbits, -1 do
  470. local nip = value:apply_mask(i):tostring() .. "/" .. i
  471. srch[#srch + 1] = nip
  472. end
  473. else
  474. srch[#srch + 1] = value
  475. end
  476. local function redis_map_cb(err, data)
  477. lua_util.debugm(N, task, 'got reply from Redis when trying to get key %s: err=%s, data=%s',
  478. key, err, data)
  479. if not err and type(data) ~= 'userdata' then
  480. callback(data)
  481. end
  482. end
  483. return rspamd_redis_make_request(task,
  484. redis_params, -- connect params
  485. key, -- hash key
  486. false, -- is write
  487. redis_map_cb, --callback
  488. cmd, -- command
  489. srch -- arguments
  490. )
  491. end
  492. local function multimap_callback(task, rule)
  493. local function match_element(r, value, callback)
  494. if not value then
  495. return false
  496. end
  497. local function get_key_callback(ret, err_or_data, err_code)
  498. lua_util.debugm(N, task, 'got return "%s" (err code = %s) for multimap %s',
  499. err_or_data,
  500. err_code,
  501. rule.symbol)
  502. if ret then
  503. if type(err_or_data) == 'table' then
  504. for _, elt in ipairs(err_or_data) do
  505. callback(elt)
  506. end
  507. else
  508. callback(err_or_data)
  509. end
  510. elseif err_code ~= 404 then
  511. rspamd_logger.infox(task, "map %s: get key returned error %s: %s",
  512. rule.symbol, err_code, err_or_data)
  513. end
  514. end
  515. lua_util.debugm(N, task, 'check value %s for multimap %s', value,
  516. rule.symbol)
  517. local ret = false
  518. if r.redis_key then
  519. -- Deal with hash name here: it can be either plain string or a selector
  520. if type(r.redis_key) == 'string' then
  521. ret = multimap_query_redis(r.redis_key, task, value, callback)
  522. else
  523. -- Here we have a selector
  524. local results = r.redis_key(task)
  525. -- Here we need to spill this function into multiple queries
  526. if type(results) == 'table' then
  527. for _, res in ipairs(results) do
  528. ret = multimap_query_redis(res, task, value, callback)
  529. if not ret then
  530. break
  531. end
  532. end
  533. else
  534. ret = multimap_query_redis(results, task, value, callback)
  535. end
  536. end
  537. return ret
  538. elseif r.map_obj then
  539. r.map_obj:get_key(value, get_key_callback, task)
  540. end
  541. end
  542. local function insert_results(result, opt)
  543. local _, symbol, score, opts = parse_multimap_value(rule, result)
  544. local forced = false
  545. if symbol then
  546. if rule.symbols_set then
  547. if not rule.symbols_set[symbol] then
  548. rspamd_logger.infox(task, 'symbol %s is not registered for map %s, ' ..
  549. 'replace it with just %s',
  550. symbol, rule.symbol, rule.symbol)
  551. symbol = rule.symbol
  552. end
  553. elseif rule.disable_multisymbol then
  554. symbol = rule.symbol
  555. if type(opt) == 'table' then
  556. table.insert(opt, result)
  557. elseif type(opt) ~= nil then
  558. opt = { opt, result }
  559. else
  560. opt = { result }
  561. end
  562. else
  563. forced = not rule.dynamic_symbols
  564. end
  565. else
  566. symbol = rule.symbol
  567. end
  568. if opts and #opts > 0 then
  569. -- Options come from the map itself
  570. task:insert_result(forced, symbol, score, opts)
  571. else
  572. if opt then
  573. if type(opt) == 'table' then
  574. task:insert_result(forced, symbol, score, fun.totable(fun.map(tostring, opt)))
  575. else
  576. task:insert_result(forced, symbol, score, tostring(opt))
  577. end
  578. else
  579. task:insert_result(forced, symbol, score)
  580. end
  581. end
  582. if rule.action then
  583. local message = rule.message
  584. if rule.message_func then
  585. message = rule.message_func(task, rule.symbol, opt)
  586. end
  587. if message then
  588. task:set_pre_result(rule.action, message, N)
  589. else
  590. task:set_pre_result(rule.action, 'Matched map: ' .. rule.symbol, N)
  591. end
  592. end
  593. end
  594. -- Match a single value for against a single rule
  595. local function match_rule(r, value)
  596. local function rule_callback(result)
  597. if result then
  598. if type(result) == 'table' then
  599. for _, rs in ipairs(result) do
  600. if type(rs) ~= 'userdata' then
  601. rule_callback(rs)
  602. end
  603. end
  604. return
  605. end
  606. local opt = value_types[r['type']].get_value(value)
  607. insert_results(result, opt)
  608. end
  609. end
  610. if r.filter or r.type == 'url' then
  611. local fn = multimap_filters[r.type]
  612. if fn then
  613. local filtered_value = fn(task, r.filter, value, r)
  614. lua_util.debugm(N, task, 'apply filter %s for rule %s: %s -> %s',
  615. r.filter, r.symbol, value, filtered_value)
  616. value = filtered_value
  617. end
  618. end
  619. if type(value) == 'table' then
  620. fun.each(function(elt)
  621. match_element(r, elt, rule_callback)
  622. end, value)
  623. else
  624. match_element(r, value, rule_callback)
  625. end
  626. end
  627. -- Match list of values according to the field
  628. local function match_list(r, ls, fields)
  629. if ls then
  630. if fields then
  631. fun.each(function(e)
  632. local match = e[fields[1]]
  633. if match then
  634. if fields[2] then
  635. match = fields[2](match)
  636. end
  637. match_rule(r, match)
  638. end
  639. end, ls)
  640. else
  641. fun.each(function(e)
  642. match_rule(r, e)
  643. end, ls)
  644. end
  645. end
  646. end
  647. local function match_addr(r, addr)
  648. match_list(r, addr, { 'addr' })
  649. if not r.filter then
  650. match_list(r, addr, { 'domain' })
  651. match_list(r, addr, { 'user' })
  652. end
  653. end
  654. local function match_url(r, url)
  655. match_rule(r, url)
  656. end
  657. local function match_hostname(r, hostname)
  658. match_rule(r, hostname)
  659. end
  660. local function match_filename(r, fn)
  661. match_rule(r, fn)
  662. end
  663. local function match_received_header(r, pos, total, h)
  664. local use_tld = false
  665. local filter = r['filter'] or 'real_ip'
  666. if filter:match('^tld:') then
  667. filter = filter:sub(5)
  668. use_tld = true
  669. end
  670. local v = h[filter]
  671. if v then
  672. local min_pos = tonumber(r['min_pos'])
  673. local max_pos = tonumber(r['max_pos'])
  674. if min_pos then
  675. if min_pos < 0 then
  676. if min_pos == -1 then
  677. if (pos ~= total) then
  678. return
  679. end
  680. else
  681. if pos <= (total - (min_pos * -1)) then
  682. return
  683. end
  684. end
  685. elseif pos < min_pos then
  686. return
  687. end
  688. end
  689. if max_pos then
  690. if max_pos < -1 then
  691. if (total - (max_pos * -1)) >= pos then
  692. return
  693. end
  694. elseif max_pos > 0 then
  695. if pos > max_pos then
  696. return
  697. end
  698. end
  699. end
  700. local match_flags = r['flags']
  701. local nmatch_flags = r['nflags']
  702. if match_flags or nmatch_flags then
  703. local got_flags = h['flags']
  704. if match_flags then
  705. for _, flag in ipairs(match_flags) do
  706. if not got_flags[flag] then
  707. return
  708. end
  709. end
  710. end
  711. if nmatch_flags then
  712. for _, flag in ipairs(nmatch_flags) do
  713. if got_flags[flag] then
  714. return
  715. end
  716. end
  717. end
  718. end
  719. if filter == 'real_ip' or filter == 'from_ip' then
  720. if type(v) == 'string' then
  721. v = rspamd_ip.from_string(v)
  722. end
  723. if v and v:is_valid() then
  724. match_rule(r, v)
  725. end
  726. else
  727. if use_tld and type(v) == 'string' then
  728. v = rspamd_util.get_tld(v)
  729. end
  730. match_rule(r, v)
  731. end
  732. end
  733. end
  734. local function match_content(r)
  735. local data
  736. if r['filter'] then
  737. data = apply_content_filter(task, r['filter'], r)
  738. else
  739. data = { task:get_content() }
  740. end
  741. for _, v in ipairs(data) do
  742. match_rule(r, v)
  743. end
  744. end
  745. if rule.expression and not rule.combined then
  746. local res, trace = rule['expression']:process_traced(task)
  747. if not res or res == 0 then
  748. lua_util.debugm(N, task, 'condition is false for %s',
  749. rule.symbol)
  750. return
  751. else
  752. lua_util.debugm(N, task, 'condition is true for %s: %s',
  753. rule.symbol,
  754. trace)
  755. end
  756. end
  757. local process_rule_funcs = {
  758. ip = function()
  759. local ip = task:get_from_ip()
  760. if ip and ip:is_valid() then
  761. match_rule(rule, ip)
  762. end
  763. end,
  764. dnsbl = function()
  765. local ip = task:get_from_ip()
  766. if ip and ip:is_valid() then
  767. local to_resolve = ip_to_rbl(ip, rule['map'])
  768. local function dns_cb(_, _, results, err)
  769. lua_util.debugm(N, rspamd_config,
  770. 'resolve() finished: results=%1, err=%2, to_resolve=%3',
  771. results, err, to_resolve)
  772. if err and
  773. (err ~= 'requested record is not found' and
  774. err ~= 'no records with this name') then
  775. rspamd_logger.errx(task, 'error looking up %s: %s', to_resolve, results)
  776. elseif results then
  777. task:insert_result(rule['symbol'], 1, rule['map'])
  778. if rule.action then
  779. task:set_pre_result(rule['action'],
  780. 'Matched map: ' .. rule['symbol'], N)
  781. end
  782. end
  783. end
  784. task:get_resolver():resolve_a({
  785. task = task,
  786. name = to_resolve,
  787. callback = dns_cb,
  788. forced = true
  789. })
  790. end
  791. end,
  792. header = function()
  793. if type(rule['header']) == 'table' then
  794. for _, rh in ipairs(rule['header']) do
  795. local hv = task:get_header_full(rh)
  796. match_list(rule, hv, { 'decoded' })
  797. end
  798. else
  799. local hv = task:get_header_full(rule['header'])
  800. match_list(rule, hv, { 'decoded' })
  801. end
  802. end,
  803. rcpt = function()
  804. local extract_from = rule.extract_from or 'default'
  805. if extract_from == 'mime' then
  806. local rcpts = task:get_recipients('mime')
  807. if rcpts then
  808. lua_util.debugm(N, task, 'checking mime rcpts against the map')
  809. match_addr(rule, rcpts)
  810. end
  811. elseif extract_from == 'smtp' then
  812. local rcpts = task:get_recipients('smtp')
  813. if rcpts then
  814. lua_util.debugm(N, task, 'checking smtp rcpts against the map')
  815. match_addr(rule, rcpts)
  816. end
  817. elseif extract_from == 'both' then
  818. local rcpts = task:get_recipients('smtp')
  819. if rcpts then
  820. lua_util.debugm(N, task, 'checking smtp rcpts against the map')
  821. match_addr(rule, rcpts)
  822. end
  823. rcpts = task:get_recipients('mime')
  824. if rcpts then
  825. lua_util.debugm(N, task, 'checking mime rcpts against the map')
  826. match_addr(rule, rcpts)
  827. end
  828. else
  829. -- Default algorithm
  830. if task:has_recipients('smtp') then
  831. local rcpts = task:get_recipients('smtp')
  832. lua_util.debugm(N, task, 'checking smtp rcpts against the map')
  833. match_addr(rule, rcpts)
  834. elseif task:has_recipients('mime') then
  835. local rcpts = task:get_recipients('mime')
  836. lua_util.debugm(N, task, 'checking mime rcpts against the map')
  837. match_addr(rule, rcpts)
  838. end
  839. end
  840. end,
  841. from = function()
  842. local extract_from = rule.extract_from or 'default'
  843. if extract_from == 'mime' then
  844. local from = task:get_from('mime')
  845. if from then
  846. lua_util.debugm(N, task, 'checking mime from against the map')
  847. match_addr(rule, from)
  848. end
  849. elseif extract_from == 'smtp' then
  850. local from = task:get_from('smtp')
  851. if from then
  852. lua_util.debugm(N, task, 'checking smtp from against the map')
  853. match_addr(rule, from)
  854. end
  855. elseif extract_from == 'both' then
  856. local from = task:get_from('smtp')
  857. if from then
  858. lua_util.debugm(N, task, 'checking smtp from against the map')
  859. match_addr(rule, from)
  860. end
  861. from = task:get_from('mime')
  862. if from then
  863. lua_util.debugm(N, task, 'checking mime from against the map')
  864. match_addr(rule, from)
  865. end
  866. else
  867. -- Default algorithm
  868. if task:has_from('smtp') then
  869. local from = task:get_from('smtp')
  870. lua_util.debugm(N, task, 'checking smtp from against the map')
  871. match_addr(rule, from)
  872. elseif task:has_from('mime') then
  873. local from = task:get_from('mime')
  874. lua_util.debugm(N, task, 'checking mime from against the map')
  875. match_addr(rule, from)
  876. end
  877. end
  878. end,
  879. helo = function()
  880. local helo = task:get_helo()
  881. if helo then
  882. match_hostname(rule, helo)
  883. end
  884. end,
  885. url = function()
  886. if task:has_urls() then
  887. local msg_urls = task:get_urls()
  888. for _, url in ipairs(msg_urls) do
  889. match_url(rule, url)
  890. end
  891. end
  892. end,
  893. user = function()
  894. local user = task:get_user()
  895. if user then
  896. match_rule(rule, user)
  897. end
  898. end,
  899. filename = function()
  900. local parts = task:get_parts()
  901. local function filter_parts(p)
  902. return p:is_attachment() or (not p:is_text()) and (not p:is_multipart())
  903. end
  904. local function filter_archive(p)
  905. local ext = p:get_detected_ext()
  906. local det_type = 'unknown'
  907. if ext then
  908. local lua_magic_types = require "lua_magic/types"
  909. local det_t = lua_magic_types[ext]
  910. if det_t then
  911. det_type = det_t.type
  912. end
  913. end
  914. return p:is_archive() and det_type == 'archive' and not rule.skip_archives
  915. end
  916. for _, p in fun.iter(fun.filter(filter_parts, parts)) do
  917. if filter_archive(p) then
  918. local fnames = p:get_archive():get_files(1000)
  919. for _, fn in ipairs(fnames) do
  920. match_filename(rule, fn)
  921. end
  922. end
  923. local fn = p:get_filename()
  924. if fn then
  925. match_filename(rule, fn)
  926. end
  927. -- Also deal with detected content type
  928. if not rule.skip_detected then
  929. local ext = p:get_detected_ext()
  930. if ext then
  931. local fake_fname = string.format('detected.%s', ext)
  932. lua_util.debugm(N, task, 'detected filename %s',
  933. fake_fname)
  934. match_filename(rule, fake_fname)
  935. end
  936. end
  937. end
  938. end,
  939. content = function()
  940. match_content(rule)
  941. end,
  942. hostname = function()
  943. local hostname = task:get_hostname()
  944. if hostname then
  945. match_hostname(rule, hostname)
  946. end
  947. end,
  948. asn = function()
  949. local asn = task:get_mempool():get_variable('asn')
  950. if asn then
  951. match_rule(rule, asn)
  952. end
  953. end,
  954. country = function()
  955. local country = task:get_mempool():get_variable('country')
  956. if country then
  957. match_rule(rule, country)
  958. end
  959. end,
  960. mempool = function()
  961. local var = task:get_mempool():get_variable(rule['variable'])
  962. if var then
  963. match_rule(rule, var)
  964. end
  965. end,
  966. symbol_options = function()
  967. local sym = task:get_symbol(rule['target_symbol'])
  968. if sym and sym[1].options then
  969. for _, o in ipairs(sym[1].options) do
  970. match_rule(rule, o)
  971. end
  972. end
  973. end,
  974. received = function()
  975. local hdrs = task:get_received_headers()
  976. if hdrs and hdrs[1] then
  977. if not rule['artificial'] then
  978. hdrs = fun.filter(function(h)
  979. return not h['flags']['artificial']
  980. end, hdrs):totable()
  981. end
  982. for pos, h in ipairs(hdrs) do
  983. match_received_header(rule, pos, #hdrs, h)
  984. end
  985. end
  986. end,
  987. selector = function()
  988. local elts = rule.selector(task)
  989. if elts then
  990. if type(elts) == 'table' then
  991. for _, elt in ipairs(elts) do
  992. match_rule(rule, elt)
  993. end
  994. else
  995. match_rule(rule, elts)
  996. end
  997. end
  998. end,
  999. combined = function()
  1000. local ret, trace = rule.combined:process(task)
  1001. if ret and ret ~= 0 then
  1002. for n, t in pairs(trace) do
  1003. insert_results(t.value, string.format("%s=%s",
  1004. n, t.matched))
  1005. end
  1006. end
  1007. end,
  1008. }
  1009. local rt = rule.type
  1010. local process_func = process_rule_funcs[rt]
  1011. if process_func then
  1012. process_func()
  1013. else
  1014. rspamd_logger.errx(task, 'Unrecognised rule type: %s', rt)
  1015. end
  1016. end
  1017. local function gen_multimap_callback(rule)
  1018. return function(task)
  1019. multimap_callback(task, rule)
  1020. end
  1021. end
  1022. local function multimap_on_load_gen(rule)
  1023. return function()
  1024. lua_util.debugm(N, rspamd_config, "loaded map object for rule %s", rule['symbol'])
  1025. local known_symbols = {}
  1026. rule.map_obj:foreach(function(key, value)
  1027. local r, symbol, score, _ = parse_multimap_value(rule, value)
  1028. if r and symbol and not known_symbols[symbol] then
  1029. lua_util.debugm(N, rspamd_config, "%s: adding new symbol %s (score = %s), triggered by %s",
  1030. rule.symbol, symbol, score, key)
  1031. rspamd_config:register_symbol {
  1032. name = value,
  1033. parent = rule.callback_id,
  1034. type = 'virtual',
  1035. score = score,
  1036. }
  1037. rspamd_config:set_metric_symbol({
  1038. group = N,
  1039. score = 1.0, -- In future, we will parse score from `get_value` and use it as multiplier
  1040. description = 'Automatic symbol generated by rule: ' .. rule.symbol,
  1041. name = value,
  1042. })
  1043. known_symbols[value] = true
  1044. end
  1045. end)
  1046. end
  1047. end
  1048. local function add_multimap_rule(key, newrule)
  1049. local ret = false
  1050. local function multimap_load_kv_map(rule)
  1051. if rule['regexp'] then
  1052. if rule['multi'] then
  1053. rule.map_obj = lua_maps.map_add_from_ucl(rule.map, 'regexp_multi',
  1054. rule.description)
  1055. else
  1056. rule.map_obj = lua_maps.map_add_from_ucl(rule.map, 'regexp',
  1057. rule.description)
  1058. end
  1059. elseif rule['glob'] then
  1060. if rule['multi'] then
  1061. rule.map_obj = lua_maps.map_add_from_ucl(rule.map, 'glob_multi',
  1062. rule.description)
  1063. else
  1064. rule.map_obj = lua_maps.map_add_from_ucl(rule.map, 'glob',
  1065. rule.description)
  1066. end
  1067. else
  1068. rule.map_obj = lua_maps.map_add_from_ucl(rule.map, 'hash',
  1069. rule.description)
  1070. end
  1071. end
  1072. local known_generic_types = {
  1073. header = true,
  1074. rcpt = true,
  1075. from = true,
  1076. helo = true,
  1077. symbol_options = true,
  1078. filename = true,
  1079. url = true,
  1080. user = true,
  1081. content = true,
  1082. hostname = true,
  1083. asn = true,
  1084. country = true,
  1085. mempool = true,
  1086. selector = true,
  1087. combined = true
  1088. }
  1089. if newrule['message_func'] then
  1090. newrule['message_func'] = assert(load(newrule['message_func']))()
  1091. end
  1092. if newrule['url'] and not newrule['map'] then
  1093. newrule['map'] = newrule['url']
  1094. end
  1095. if not (newrule.map or newrule.rules) then
  1096. rspamd_logger.errx(rspamd_config, 'incomplete rule, missing map')
  1097. return nil
  1098. end
  1099. if not newrule['symbol'] and key then
  1100. newrule['symbol'] = key
  1101. elseif not newrule['symbol'] then
  1102. rspamd_logger.errx(rspamd_config, 'incomplete rule, missing symbol')
  1103. return nil
  1104. end
  1105. if not newrule['description'] then
  1106. newrule['description'] = string.format('multimap, type %s: %s', newrule['type'],
  1107. newrule['symbol'])
  1108. end
  1109. if newrule['type'] == 'mempool' and not newrule['variable'] then
  1110. rspamd_logger.errx(rspamd_config, 'mempool map requires variable')
  1111. return nil
  1112. end
  1113. if newrule['type'] == 'selector' then
  1114. if not newrule['selector'] then
  1115. rspamd_logger.errx(rspamd_config, 'selector map requires selector definition')
  1116. return nil
  1117. else
  1118. local selector = lua_selectors.create_selector_closure(
  1119. rspamd_config, newrule['selector'], newrule['delimiter'] or "")
  1120. if not selector then
  1121. rspamd_logger.errx(rspamd_config, 'selector map has invalid selector: "%s", symbol: %s',
  1122. newrule['selector'], newrule['symbol'])
  1123. return nil
  1124. end
  1125. newrule.selector = selector
  1126. end
  1127. end
  1128. if type(newrule['map']) == 'string' and
  1129. string.find(newrule['map'], '^redis://.*$') then
  1130. if not redis_params then
  1131. rspamd_logger.infox(rspamd_config, 'no redis servers are specified, ' ..
  1132. 'cannot add redis map %s: %s', newrule['symbol'], newrule['map'])
  1133. return nil
  1134. end
  1135. newrule['redis_key'] = string.match(newrule['map'], '^redis://(.*)$')
  1136. if newrule['redis_key'] then
  1137. ret = true
  1138. end
  1139. elseif type(newrule['map']) == 'string' and
  1140. string.find(newrule['map'], '^redis%+selector://.*$') then
  1141. if not redis_params then
  1142. rspamd_logger.infox(rspamd_config, 'no redis servers are specified, ' ..
  1143. 'cannot add redis map %s: %s', newrule['symbol'], newrule['map'])
  1144. return nil
  1145. end
  1146. local selector_str = string.match(newrule['map'], '^redis%+selector://(.*)$')
  1147. local selector = lua_selectors.create_selector_closure(
  1148. rspamd_config, selector_str, newrule['delimiter'] or "")
  1149. if not selector then
  1150. rspamd_logger.errx(rspamd_config, 'redis selector map has invalid selector: "%s", symbol: %s',
  1151. selector_str, newrule['symbol'])
  1152. return nil
  1153. end
  1154. newrule['redis_key'] = selector
  1155. ret = true
  1156. elseif newrule.type == 'combined' then
  1157. local lua_maps_expressions = require "lua_maps_expressions"
  1158. newrule.combined = lua_maps_expressions.create(rspamd_config,
  1159. {
  1160. rules = newrule.rules,
  1161. expression = newrule.expression,
  1162. on_load = newrule.dynamic_symbols and multimap_on_load_gen(newrule) or nil,
  1163. }, N, 'Combined map for ' .. newrule.symbol)
  1164. if not newrule.combined then
  1165. rspamd_logger.errx(rspamd_config, 'cannot add combined map for %s', newrule.symbol)
  1166. else
  1167. ret = true
  1168. end
  1169. else
  1170. if newrule['type'] == 'ip' then
  1171. newrule.map_obj = lua_maps.map_add_from_ucl(newrule.map, 'radix',
  1172. newrule.description)
  1173. if newrule.map_obj then
  1174. ret = true
  1175. else
  1176. rspamd_logger.warnx(rspamd_config, 'Cannot add rule: map doesn\'t exists: %1',
  1177. newrule['map'])
  1178. end
  1179. elseif newrule['type'] == 'received' then
  1180. if type(newrule['flags']) == 'table' and newrule['flags'][1] then
  1181. newrule['flags'] = newrule['flags']
  1182. elseif type(newrule['flags']) == 'string' then
  1183. newrule['flags'] = { newrule['flags'] }
  1184. end
  1185. if type(newrule['nflags']) == 'table' and newrule['nflags'][1] then
  1186. newrule['nflags'] = newrule['nflags']
  1187. elseif type(newrule['nflags']) == 'string' then
  1188. newrule['nflags'] = { newrule['nflags'] }
  1189. end
  1190. local filter = newrule['filter'] or 'real_ip'
  1191. if filter == 'real_ip' or filter == 'from_ip' then
  1192. newrule.map_obj = lua_maps.map_add_from_ucl(newrule.map, 'radix',
  1193. newrule.description)
  1194. if newrule.map_obj then
  1195. ret = true
  1196. else
  1197. rspamd_logger.warnx(rspamd_config, 'Cannot add rule: map doesn\'t exists: %1',
  1198. newrule['map'])
  1199. end
  1200. else
  1201. multimap_load_kv_map(newrule)
  1202. if newrule.map_obj then
  1203. ret = true
  1204. else
  1205. rspamd_logger.warnx(rspamd_config, 'Cannot add rule: map doesn\'t exists: %1',
  1206. newrule['map'])
  1207. end
  1208. end
  1209. elseif known_generic_types[newrule.type] then
  1210. if newrule.filter == 'ip_addr' then
  1211. newrule.map_obj = lua_maps.map_add_from_ucl(newrule.map, 'radix',
  1212. newrule.description)
  1213. elseif not newrule.combined then
  1214. multimap_load_kv_map(newrule)
  1215. end
  1216. if newrule.map_obj then
  1217. ret = true
  1218. else
  1219. rspamd_logger.warnx(rspamd_config, 'Cannot add rule: map doesn\'t exists: %1',
  1220. newrule['map'])
  1221. end
  1222. elseif newrule['type'] == 'dnsbl' then
  1223. ret = true
  1224. end
  1225. end
  1226. if ret then
  1227. if newrule.map_obj and newrule.dynamic_symbols then
  1228. newrule.map_obj:on_load(multimap_on_load_gen(newrule))
  1229. end
  1230. if newrule['type'] == 'symbol_options' then
  1231. rspamd_config:register_dependency(newrule['symbol'], newrule['target_symbol'])
  1232. end
  1233. if newrule['require_symbols'] then
  1234. local atoms = {}
  1235. local function parse_atom(str)
  1236. local atom = table.concat(fun.totable(fun.take_while(function(c)
  1237. if string.find(', \t()><+!|&\n', c, 1, true) then
  1238. return false
  1239. end
  1240. return true
  1241. end, fun.iter(str))), '')
  1242. table.insert(atoms, atom)
  1243. return atom
  1244. end
  1245. local function process_atom(atom, task)
  1246. local f_ret = task:has_symbol(atom)
  1247. lua_util.debugm(N, rspamd_config, 'check for symbol %s: %s', atom, f_ret)
  1248. if f_ret then
  1249. return 1
  1250. end
  1251. return 0
  1252. end
  1253. local expression = rspamd_expression.create(newrule['require_symbols'],
  1254. { parse_atom, process_atom }, rspamd_config:get_mempool())
  1255. if expression then
  1256. newrule['expression'] = expression
  1257. fun.each(function(v)
  1258. lua_util.debugm(N, rspamd_config, 'add dependency %s -> %s',
  1259. newrule['symbol'], v)
  1260. rspamd_config:register_dependency(newrule['symbol'], v)
  1261. end, atoms)
  1262. end
  1263. end
  1264. return newrule
  1265. end
  1266. return nil
  1267. end
  1268. -- Registration
  1269. local opts = rspamd_config:get_all_opt(N)
  1270. if opts and type(opts) == 'table' then
  1271. redis_params = rspamd_parse_redis_server(N)
  1272. for k, m in pairs(opts) do
  1273. if type(m) == 'table' and m['type'] then
  1274. local rule = add_multimap_rule(k, m)
  1275. if not rule then
  1276. rspamd_logger.errx(rspamd_config, 'cannot add rule: "' .. k .. '"')
  1277. else
  1278. rspamd_logger.infox(rspamd_config, 'added multimap rule: %s (%s)',
  1279. k, rule.type)
  1280. table.insert(rules, rule)
  1281. end
  1282. end
  1283. end
  1284. -- add fake symbol to check all maps inside a single callback
  1285. fun.each(function(rule)
  1286. local augmentations = {}
  1287. if rule.action then
  1288. table.insert(augmentations, 'passthrough')
  1289. end
  1290. local id = rspamd_config:register_symbol({
  1291. type = 'normal',
  1292. name = rule['symbol'],
  1293. augmentations = augmentations,
  1294. callback = gen_multimap_callback(rule),
  1295. })
  1296. rule.callback_id = id
  1297. if rule['symbols'] then
  1298. -- Find allowed symbols by this map
  1299. rule['symbols_set'] = {}
  1300. fun.each(function(s)
  1301. rspamd_config:register_symbol({
  1302. type = 'virtual',
  1303. name = s,
  1304. parent = id,
  1305. score = tonumber(rule.score or "0") or 0, -- Default score
  1306. })
  1307. rule['symbols_set'][s] = 1
  1308. end, rule['symbols'])
  1309. end
  1310. if not rule.score then
  1311. rspamd_logger.infox(rspamd_config, 'set default score 0 for multimap rule %s', rule.symbol)
  1312. rule.score = 0
  1313. end
  1314. if rule.score then
  1315. -- Register metric symbol
  1316. rule.name = rule.symbol
  1317. rule.description = rule.description or 'multimap symbol'
  1318. rule.group = rule.group or N
  1319. local tmp_flags
  1320. tmp_flags = rule.flags
  1321. if rule.type == 'received' and rule.flags then
  1322. -- XXX: hack to allow received flags/nflags
  1323. -- See issue #3526 on GH
  1324. rule.flags = nil
  1325. end
  1326. -- XXX: for combined maps we use trace, so flags must include one_shot to avoid scores multiplication
  1327. if rule.combined and not rule.flags then
  1328. rule.flags = 'one_shot'
  1329. end
  1330. rspamd_config:set_metric_symbol(rule)
  1331. rule.flags = tmp_flags
  1332. end
  1333. end, rules)
  1334. if #rules == 0 then
  1335. lua_util.disable_module(N, "config")
  1336. end
  1337. end