rspamd.local.lua 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948
  1. rspamd_config.MAILCOW_AUTH = {
  2. callback = function(task)
  3. local uname = task:get_user()
  4. if uname then
  5. return 1
  6. end
  7. end
  8. }
  9. local monitoring_hosts = rspamd_config:add_map{
  10. url = "/etc/rspamd/custom/monitoring_nolog.map",
  11. description = "Monitoring hosts",
  12. type = "regexp"
  13. }
  14. rspamd_config:register_symbol({
  15. name = 'SMTP_ACCESS',
  16. type = 'postfilter',
  17. callback = function(task)
  18. local util = require("rspamd_util")
  19. local rspamd_logger = require "rspamd_logger"
  20. local rspamd_ip = require 'rspamd_ip'
  21. local uname = task:get_user()
  22. local limited_access = task:get_symbol("SMTP_LIMITED_ACCESS")
  23. if not uname then
  24. return false
  25. end
  26. if not limited_access then
  27. return false
  28. end
  29. local hash_key = 'SMTP_ALLOW_NETS_' .. uname
  30. local redis_params = rspamd_parse_redis_server('smtp_access')
  31. local ip = task:get_from_ip()
  32. if ip == nil or not ip:is_valid() then
  33. return false
  34. end
  35. local from_ip_string = tostring(ip)
  36. smtp_access_table = {from_ip_string}
  37. local maxbits = 128
  38. local minbits = 32
  39. if ip:get_version() == 4 then
  40. maxbits = 32
  41. minbits = 8
  42. end
  43. for i=maxbits,minbits,-1 do
  44. local nip = ip:apply_mask(i):to_string() .. "/" .. i
  45. table.insert(smtp_access_table, nip)
  46. end
  47. local function smtp_access_cb(err, data)
  48. if err then
  49. rspamd_logger.infox(rspamd_config, "smtp_access query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
  50. return false
  51. else
  52. rspamd_logger.infox(rspamd_config, "checking ip %s for smtp_access in %s", from_ip_string, hash_key)
  53. for k,v in pairs(data) do
  54. if (v and v ~= userdata and v == '1') then
  55. rspamd_logger.infox(rspamd_config, "found ip in smtp_access map")
  56. task:insert_result(true, 'SMTP_ACCESS', 0.0, from_ip_string)
  57. return true
  58. end
  59. end
  60. rspamd_logger.infox(rspamd_config, "couldnt find ip in smtp_access map")
  61. task:insert_result(true, 'SMTP_ACCESS', 999.0, from_ip_string)
  62. return true
  63. end
  64. end
  65. table.insert(smtp_access_table, 1, hash_key)
  66. local redis_ret_user = rspamd_redis_make_request(task,
  67. redis_params, -- connect params
  68. hash_key, -- hash key
  69. false, -- is write
  70. smtp_access_cb, --callback
  71. 'HMGET', -- command
  72. smtp_access_table -- arguments
  73. )
  74. if not redis_ret_user then
  75. rspamd_logger.infox(rspamd_config, "cannot check smtp_access redis map")
  76. end
  77. end,
  78. priority = 10
  79. })
  80. rspamd_config:register_symbol({
  81. name = 'POSTMASTER_HANDLER',
  82. type = 'prefilter',
  83. callback = function(task)
  84. local rcpts = task:get_recipients('smtp')
  85. local rspamd_logger = require "rspamd_logger"
  86. local lua_util = require "lua_util"
  87. local from = task:get_from(1)
  88. -- not applying to mails with more than one rcpt to avoid bypassing filters by addressing postmaster
  89. if rcpts and #rcpts == 1 then
  90. for _,rcpt in ipairs(rcpts) do
  91. local rcpt_split = rspamd_str_split(rcpt['addr'], '@')
  92. if #rcpt_split == 2 then
  93. if rcpt_split[1] == 'postmaster' then
  94. task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt', 'postmaster')
  95. return
  96. end
  97. end
  98. end
  99. end
  100. if from then
  101. for _,fr in ipairs(from) do
  102. local fr_split = rspamd_str_split(fr['addr'], '@')
  103. if #fr_split == 2 then
  104. if fr_split[1] == 'postmaster' and task:get_user() then
  105. -- no whitelist, keep signatures
  106. task:insert_result(true, 'POSTMASTER_FROM', -2500.0)
  107. return
  108. end
  109. end
  110. end
  111. end
  112. end,
  113. priority = 10
  114. })
  115. rspamd_config:register_symbol({
  116. name = 'KEEP_SPAM',
  117. type = 'prefilter',
  118. callback = function(task)
  119. local util = require("rspamd_util")
  120. local rspamd_logger = require "rspamd_logger"
  121. local rspamd_ip = require 'rspamd_ip'
  122. local uname = task:get_user()
  123. if uname then
  124. return false
  125. end
  126. local redis_params = rspamd_parse_redis_server('keep_spam')
  127. local ip = task:get_from_ip()
  128. if ip == nil or not ip:is_valid() then
  129. return false
  130. end
  131. -- Helper function to parse IPv6 into 8 segments
  132. local function ipv6_to_segments(ip_str)
  133. -- Remove zone identifier if present (e.g., %eth0)
  134. ip_str = ip_str:gsub("%%.*$", "")
  135. local segments = {}
  136. -- Handle :: compression
  137. if ip_str:find('::') then
  138. local before, after = ip_str:match('^(.*)::(.*)$')
  139. before = before or ''
  140. after = after or ''
  141. local before_parts = {}
  142. local after_parts = {}
  143. if before ~= '' then
  144. for seg in before:gmatch('[^:]+') do
  145. table.insert(before_parts, tonumber(seg, 16) or 0)
  146. end
  147. end
  148. if after ~= '' then
  149. for seg in after:gmatch('[^:]+') do
  150. table.insert(after_parts, tonumber(seg, 16) or 0)
  151. end
  152. end
  153. -- Add before segments
  154. for _, seg in ipairs(before_parts) do
  155. table.insert(segments, seg)
  156. end
  157. -- Add compressed zeros
  158. local zeros_needed = 8 - #before_parts - #after_parts
  159. for i = 1, zeros_needed do
  160. table.insert(segments, 0)
  161. end
  162. -- Add after segments
  163. for _, seg in ipairs(after_parts) do
  164. table.insert(segments, seg)
  165. end
  166. else
  167. -- No compression
  168. for seg in ip_str:gmatch('[^:]+') do
  169. table.insert(segments, tonumber(seg, 16) or 0)
  170. end
  171. end
  172. -- Ensure we have exactly 8 segments
  173. while #segments < 8 do
  174. table.insert(segments, 0)
  175. end
  176. return segments
  177. end
  178. -- Generate all common IPv6 notations
  179. local function get_ipv6_variants(ip_str)
  180. local variants = {}
  181. local seen = {}
  182. local function add_variant(v)
  183. if v and not seen[v] then
  184. table.insert(variants, v)
  185. seen[v] = true
  186. end
  187. end
  188. -- For IPv4, just return the original
  189. if not ip_str:find(':') then
  190. add_variant(ip_str)
  191. return variants
  192. end
  193. local segments = ipv6_to_segments(ip_str)
  194. -- 1. Fully expanded form (all zeros shown as 0000)
  195. local expanded_parts = {}
  196. for _, seg in ipairs(segments) do
  197. table.insert(expanded_parts, string.format('%04x', seg))
  198. end
  199. add_variant(table.concat(expanded_parts, ':'))
  200. -- 2. Standard form (no leading zeros, but all segments present)
  201. local standard_parts = {}
  202. for _, seg in ipairs(segments) do
  203. table.insert(standard_parts, string.format('%x', seg))
  204. end
  205. add_variant(table.concat(standard_parts, ':'))
  206. -- 3. Find all possible :: compressions
  207. -- RFC 5952: compress the longest run of consecutive zeros
  208. -- But we need to check all possibilities since Redis might have any form
  209. -- Find all zero runs
  210. local zero_runs = {}
  211. local in_run = false
  212. local run_start = 0
  213. local run_length = 0
  214. for i = 1, 8 do
  215. if segments[i] == 0 then
  216. if not in_run then
  217. in_run = true
  218. run_start = i
  219. run_length = 1
  220. else
  221. run_length = run_length + 1
  222. end
  223. else
  224. if in_run then
  225. if run_length >= 1 then -- Allow single zero compression too
  226. table.insert(zero_runs, {start = run_start, length = run_length})
  227. end
  228. in_run = false
  229. end
  230. end
  231. end
  232. -- Don't forget the last run
  233. if in_run and run_length >= 1 then
  234. table.insert(zero_runs, {start = run_start, length = run_length})
  235. end
  236. -- Generate variant for each zero run compression
  237. for _, run in ipairs(zero_runs) do
  238. local parts = {}
  239. -- Before compression
  240. for i = 1, run.start - 1 do
  241. table.insert(parts, string.format('%x', segments[i]))
  242. end
  243. -- The compression
  244. if run.start == 1 then
  245. table.insert(parts, '')
  246. table.insert(parts, '')
  247. elseif run.start + run.length - 1 == 8 then
  248. table.insert(parts, '')
  249. table.insert(parts, '')
  250. else
  251. table.insert(parts, '')
  252. end
  253. -- After compression
  254. for i = run.start + run.length, 8 do
  255. table.insert(parts, string.format('%x', segments[i]))
  256. end
  257. local compressed = table.concat(parts, ':'):gsub('::+', '::')
  258. add_variant(compressed)
  259. end
  260. return variants
  261. end
  262. local from_ip_string = tostring(ip)
  263. local ip_check_table = {}
  264. -- Add all variants of the exact IP
  265. for _, variant in ipairs(get_ipv6_variants(from_ip_string)) do
  266. table.insert(ip_check_table, variant)
  267. end
  268. local maxbits = 128
  269. local minbits = 32
  270. if ip:get_version() == 4 then
  271. maxbits = 32
  272. minbits = 8
  273. end
  274. -- Add all CIDR notations with variants
  275. for i=maxbits,minbits,-1 do
  276. local masked_ip = ip:apply_mask(i)
  277. local cidr_base = masked_ip:to_string()
  278. for _, variant in ipairs(get_ipv6_variants(cidr_base)) do
  279. local cidr = variant .. "/" .. i
  280. table.insert(ip_check_table, cidr)
  281. end
  282. end
  283. local function keep_spam_cb(err, data)
  284. if err then
  285. rspamd_logger.infox(rspamd_config, "keep_spam query request for ip %s returned invalid or empty data (\"%s\") or error (\"%s\")", ip, data, err)
  286. return false
  287. else
  288. for k,v in pairs(data) do
  289. if (v and v ~= userdata and v == '1') then
  290. rspamd_logger.infox(rspamd_config, "found ip %s (checked as: %s) in keep_spam map, setting pre-result accept", from_ip_string, ip_check_table[k])
  291. task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam')
  292. task:set_flag('no_stat')
  293. return
  294. end
  295. end
  296. end
  297. end
  298. table.insert(ip_check_table, 1, 'KEEP_SPAM')
  299. local redis_ret_user = rspamd_redis_make_request(task,
  300. redis_params, -- connect params
  301. 'KEEP_SPAM', -- hash key
  302. false, -- is write
  303. keep_spam_cb, --callback
  304. 'HMGET', -- command
  305. ip_check_table -- arguments
  306. )
  307. if not redis_ret_user then
  308. rspamd_logger.infox(rspamd_config, "cannot check keep_spam redis map")
  309. end
  310. end,
  311. priority = 19
  312. })
  313. rspamd_config:register_symbol({
  314. name = 'TLS_HEADER',
  315. type = 'postfilter',
  316. callback = function(task)
  317. local rspamd_logger = require "rspamd_logger"
  318. local tls_tag = task:get_request_header('TLS-Version')
  319. if type(tls_tag) == 'nil' then
  320. task:set_milter_reply({
  321. add_headers = {['X-Last-TLS-Session-Version'] = 'None'}
  322. })
  323. else
  324. task:set_milter_reply({
  325. add_headers = {['X-Last-TLS-Session-Version'] = tostring(tls_tag)}
  326. })
  327. end
  328. end,
  329. priority = 12
  330. })
  331. rspamd_config:register_symbol({
  332. name = 'TAG_MOO',
  333. type = 'postfilter',
  334. flags = 'ignore_passthrough',
  335. callback = function(task)
  336. local util = require("rspamd_util")
  337. local rspamd_logger = require "rspamd_logger"
  338. local redis_params = rspamd_parse_redis_server('taghandler')
  339. local rspamd_http = require "rspamd_http"
  340. local rcpts = task:get_recipients('smtp')
  341. local lua_util = require "lua_util"
  342. local function remove_moo_tag()
  343. local moo_tag_header = task:get_header('X-Moo-Tag', false)
  344. if moo_tag_header then
  345. task:set_milter_reply({
  346. remove_headers = {['X-Moo-Tag'] = 0},
  347. })
  348. end
  349. return true
  350. end
  351. -- Check if we have exactly one recipient
  352. if not (rcpts and #rcpts == 1) then
  353. rspamd_logger.infox("TAG_MOO: not exactly one rcpt (%s), removing moo tag", rcpts and #rcpts or 0)
  354. remove_moo_tag()
  355. return
  356. end
  357. local rcpt_addr = rcpts[1]['addr']
  358. local rcpt_user = rcpts[1]['user']
  359. local rcpt_domain = rcpts[1]['domain']
  360. -- Check if recipient has a tag (contains '+')
  361. local tag = nil
  362. if rcpt_user:find('%+') then
  363. local base_user, tag_part = rcpt_user:match('^(.-)%+(.+)$')
  364. if base_user and tag_part then
  365. tag = tag_part
  366. rspamd_logger.infox("TAG_MOO: found tag in recipient: %s (base: %s, tag: %s)", rcpt_addr, base_user, tag)
  367. end
  368. end
  369. if not tag then
  370. rspamd_logger.infox("TAG_MOO: no tag found in recipient %s, removing moo tag", rcpt_addr)
  371. remove_moo_tag()
  372. return
  373. end
  374. -- Optional: Check if domain is a mailcow domain
  375. -- When KEEP_SPAM is active, RCPT_MAILCOW_DOMAIN might not be set
  376. -- If the mail is being delivered, we can assume it's valid
  377. local mailcow_domain = task:get_symbol("RCPT_MAILCOW_DOMAIN")
  378. if not mailcow_domain then
  379. rspamd_logger.infox("TAG_MOO: RCPT_MAILCOW_DOMAIN not set (possibly due to pre-result), proceeding anyway for domain %s", rcpt_domain)
  380. end
  381. local action = task:get_metric_action('default')
  382. rspamd_logger.infox("TAG_MOO: metric action: %s", action)
  383. -- Check if we have a pre-result (e.g., from KEEP_SPAM or POSTMASTER_HANDLER)
  384. local allow_processing = false
  385. if task.has_pre_result then
  386. local has_pre, pre_action = task:has_pre_result()
  387. if has_pre then
  388. rspamd_logger.infox("TAG_MOO: pre-result detected: %s", tostring(pre_action))
  389. if pre_action == 'accept' then
  390. allow_processing = true
  391. rspamd_logger.infox("TAG_MOO: pre-result is accept, will process")
  392. end
  393. end
  394. end
  395. -- Allow processing for mild actions or when we have pre-result accept
  396. if not allow_processing and action ~= 'no action' and action ~= 'greylist' then
  397. rspamd_logger.infox("TAG_MOO: skipping tag handler for action: %s", action)
  398. remove_moo_tag()
  399. return true
  400. end
  401. rspamd_logger.infox("TAG_MOO: processing allowed")
  402. local function http_callback(err_message, code, body, headers)
  403. if body ~= nil and body ~= "" then
  404. rspamd_logger.infox(rspamd_config, "TAG_MOO: expanding rcpt to \"%s\"", body)
  405. local function tag_callback_subject(err, data)
  406. if err or type(data) ~= 'string' or data == '' then
  407. rspamd_logger.infox(rspamd_config, "TAG_MOO: subject tag handler rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying subfolder tag handler...", body, data, err)
  408. local function tag_callback_subfolder(err, data)
  409. if err or type(data) ~= 'string' or data == '' then
  410. rspamd_logger.infox(rspamd_config, "TAG_MOO: subfolder tag handler for rcpt %s returned invalid or empty data (\"%s\") or error (\"%s\")", body, data, err)
  411. remove_moo_tag()
  412. else
  413. rspamd_logger.infox("TAG_MOO: User wants subfolder tag, adding X-Moo-Tag header")
  414. task:set_milter_reply({
  415. add_headers = {['X-Moo-Tag'] = 'YES'}
  416. })
  417. end
  418. end
  419. local redis_ret_subfolder = rspamd_redis_make_request(task,
  420. redis_params, -- connect params
  421. body, -- hash key
  422. false, -- is write
  423. tag_callback_subfolder, --callback
  424. 'HGET', -- command
  425. {'RCPT_WANTS_SUBFOLDER_TAG', body} -- arguments
  426. )
  427. if not redis_ret_subfolder then
  428. rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
  429. remove_moo_tag()
  430. end
  431. else
  432. rspamd_logger.infox("TAG_MOO: user wants subject modified for tagged mail")
  433. local sbj = task:get_header('Subject') or ''
  434. new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?='
  435. task:set_milter_reply({
  436. remove_headers = {
  437. ['Subject'] = 1,
  438. ['X-Moo-Tag'] = 0
  439. },
  440. add_headers = {['Subject'] = new_sbj}
  441. })
  442. end
  443. end
  444. local redis_ret_subject = rspamd_redis_make_request(task,
  445. redis_params, -- connect params
  446. body, -- hash key
  447. false, -- is write
  448. tag_callback_subject, --callback
  449. 'HGET', -- command
  450. {'RCPT_WANTS_SUBJECT_TAG', body} -- arguments
  451. )
  452. if not redis_ret_subject then
  453. rspamd_logger.infox(rspamd_config, "TAG_MOO: cannot make request to load tag handler for rcpt")
  454. remove_moo_tag()
  455. end
  456. else
  457. rspamd_logger.infox("TAG_MOO: alias expansion returned empty body")
  458. remove_moo_tag()
  459. end
  460. end
  461. local rcpt_split = rspamd_str_split(rcpt_addr, '@')
  462. if #rcpt_split == 2 then
  463. if rcpt_split[1]:match('^postmaster') then
  464. rspamd_logger.infox(rspamd_config, "TAG_MOO: not expanding postmaster alias")
  465. remove_moo_tag()
  466. else
  467. rspamd_logger.infox("TAG_MOO: requesting alias expansion for %s", rcpt_addr)
  468. rspamd_http.request({
  469. task=task,
  470. url='http://nginx:8081/aliasexp.php',
  471. body='',
  472. callback=http_callback,
  473. headers={Rcpt=rcpt_addr},
  474. })
  475. end
  476. else
  477. rspamd_logger.infox("TAG_MOO: invalid rcpt format")
  478. remove_moo_tag()
  479. end
  480. end,
  481. priority = 19
  482. })
  483. rspamd_config:register_symbol({
  484. name = 'BCC',
  485. type = 'postfilter',
  486. flags = 'ignore_passthrough',
  487. callback = function(task)
  488. local util = require("rspamd_util")
  489. local rspamd_http = require "rspamd_http"
  490. local rspamd_logger = require "rspamd_logger"
  491. local from_table = {}
  492. local rcpt_table = {}
  493. if task:has_symbol('ENCRYPTED_CHAT') then
  494. return -- stop
  495. end
  496. local send_mail = function(task, bcc_dest)
  497. local lua_smtp = require "lua_smtp"
  498. local function sendmail_cb(ret, err)
  499. if not ret then
  500. rspamd_logger.errx(task, 'BCC SMTP ERROR: %s', err)
  501. else
  502. rspamd_logger.infox(rspamd_config, "BCC SMTP SUCCESS TO %s", bcc_dest)
  503. end
  504. end
  505. if not bcc_dest then
  506. return -- stop
  507. end
  508. -- dot stuff content before sending
  509. local email_content = tostring(task:get_content())
  510. email_content = string.gsub(email_content, "\r\n%.", "\r\n..")
  511. -- send mail
  512. local from_smtp = task:get_from('smtp')
  513. local from_addr = (from_smtp and from_smtp[1] and from_smtp[1].addr) or 'mailer-daemon@localhost'
  514. lua_smtp.sendmail({
  515. task = task,
  516. host = os.getenv("IPV4_NETWORK") .. '.253',
  517. port = 591,
  518. from = from_addr,
  519. recipients = bcc_dest,
  520. helo = 'bcc',
  521. timeout = 20,
  522. }, email_content, sendmail_cb)
  523. end
  524. -- determine from
  525. local from = task:get_from('smtp')
  526. if from then
  527. for _, a in ipairs(from) do
  528. table.insert(from_table, a['addr']) -- add this rcpt to table
  529. table.insert(from_table, '@' .. a['domain']) -- add this rcpts domain to table
  530. end
  531. else
  532. return -- stop
  533. end
  534. -- determine rcpts
  535. local rcpts = task:get_recipients('smtp')
  536. if rcpts then
  537. for _, a in ipairs(rcpts) do
  538. table.insert(rcpt_table, a['addr']) -- add this rcpt to table
  539. table.insert(rcpt_table, '@' .. a['domain']) -- add this rcpts domain to table
  540. end
  541. else
  542. return -- stop
  543. end
  544. local action = task:get_metric_action('default')
  545. rspamd_logger.infox("BCC: metric action: %s", action)
  546. -- Check for pre-result accept (e.g., from KEEP_SPAM)
  547. local allow_bcc = false
  548. if task.has_pre_result then
  549. local has_pre, pre_action = task:has_pre_result()
  550. if has_pre and pre_action == 'accept' then
  551. allow_bcc = true
  552. rspamd_logger.infox("BCC: pre-result accept detected, will send BCC")
  553. end
  554. end
  555. -- Allow BCC for mild actions or when we have pre-result accept
  556. if not allow_bcc and action ~= 'no action' and action ~= 'add header' and action ~= 'rewrite subject' then
  557. rspamd_logger.infox("BCC: skipping for action: %s", action)
  558. return
  559. end
  560. local function rcpt_callback(err_message, code, body, headers)
  561. if err_message == nil and code == 201 and body ~= nil then
  562. rspamd_logger.infox("BCC: sending BCC to %s for rcpt match", body)
  563. send_mail(task, body)
  564. end
  565. end
  566. local function from_callback(err_message, code, body, headers)
  567. if err_message == nil and code == 201 and body ~= nil then
  568. rspamd_logger.infox("BCC: sending BCC to %s for from match", body)
  569. send_mail(task, body)
  570. end
  571. end
  572. if rcpt_table then
  573. for _,e in ipairs(rcpt_table) do
  574. rspamd_logger.infox(rspamd_config, "BCC: checking bcc for rcpt address %s", e)
  575. rspamd_http.request({
  576. task=task,
  577. url='http://nginx:8081/bcc.php',
  578. body='',
  579. callback=rcpt_callback,
  580. headers={Rcpt=e}
  581. })
  582. end
  583. end
  584. if from_table then
  585. for _,e in ipairs(from_table) do
  586. rspamd_logger.infox(rspamd_config, "BCC: checking bcc for from address %s", e)
  587. rspamd_http.request({
  588. task=task,
  589. url='http://nginx:8081/bcc.php',
  590. body='',
  591. callback=from_callback,
  592. headers={From=e}
  593. })
  594. end
  595. end
  596. -- Don't return true to avoid symbol being logged
  597. end,
  598. priority = 20
  599. })
  600. rspamd_config:register_symbol({
  601. name = 'DYN_RL_CHECK',
  602. type = 'prefilter',
  603. callback = function(task)
  604. local util = require("rspamd_util")
  605. local redis_params = rspamd_parse_redis_server('dyn_rl')
  606. local rspamd_logger = require "rspamd_logger"
  607. local envfrom = task:get_from(1)
  608. local envrcpt = task:get_recipients(1) or {}
  609. local uname = task:get_user()
  610. if not envfrom or not uname then
  611. return false
  612. end
  613. local uname = uname:lower()
  614. if #envrcpt == 1 and envrcpt[1].addr:lower() == uname then
  615. return false
  616. end
  617. local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case
  618. local function redis_cb_user(err, data)
  619. if err or type(data) ~= 'string' then
  620. rspamd_logger.infox(rspamd_config, "dynamic ratelimit request for user %s returned invalid or empty data (\"%s\") or error (\"%s\") - trying dynamic ratelimit for domain...", uname, data, err)
  621. local function redis_key_cb_domain(err, data)
  622. if err or type(data) ~= 'string' then
  623. rspamd_logger.infox(rspamd_config, "dynamic ratelimit request for domain %s returned invalid or empty data (\"%s\") or error (\"%s\")", env_from_domain, data, err)
  624. else
  625. rspamd_logger.infox(rspamd_config, "found dynamic ratelimit in redis for domain %s with value %s", env_from_domain, data)
  626. task:insert_result('DYN_RL', 0.0, data, env_from_domain)
  627. end
  628. end
  629. local redis_ret_domain = rspamd_redis_make_request(task,
  630. redis_params, -- connect params
  631. env_from_domain, -- hash key
  632. false, -- is write
  633. redis_key_cb_domain, --callback
  634. 'HGET', -- command
  635. {'RL_VALUE', env_from_domain} -- arguments
  636. )
  637. if not redis_ret_domain then
  638. rspamd_logger.infox(rspamd_config, "cannot make request to load ratelimit for domain")
  639. end
  640. else
  641. rspamd_logger.infox(rspamd_config, "found dynamic ratelimit in redis for user %s with value %s", uname, data)
  642. task:insert_result('DYN_RL', 0.0, data, uname)
  643. end
  644. end
  645. local redis_ret_user = rspamd_redis_make_request(task,
  646. redis_params, -- connect params
  647. uname, -- hash key
  648. false, -- is write
  649. redis_cb_user, --callback
  650. 'HGET', -- command
  651. {'RL_VALUE', uname} -- arguments
  652. )
  653. if not redis_ret_user then
  654. rspamd_logger.infox(rspamd_config, "cannot make request to load ratelimit for user")
  655. end
  656. return true
  657. end,
  658. flags = 'empty',
  659. priority = 20
  660. })
  661. rspamd_config:register_symbol({
  662. name = 'NO_LOG_STAT',
  663. type = 'postfilter',
  664. callback = function(task)
  665. local from = task:get_header('From')
  666. if from and (monitoring_hosts:get_key(from) or from == "watchdog@localhost") then
  667. task:set_flag('no_log')
  668. task:set_flag('no_stat')
  669. end
  670. end
  671. })
  672. rspamd_config:register_symbol({
  673. name = 'MOO_FOOTER',
  674. type = 'prefilter',
  675. callback = function(task)
  676. local cjson = require "cjson"
  677. local lua_mime = require "lua_mime"
  678. local lua_util = require "lua_util"
  679. local rspamd_logger = require "rspamd_logger"
  680. local rspamd_http = require "rspamd_http"
  681. local envfrom = task:get_from(1)
  682. local uname = task:get_user()
  683. if not envfrom or not uname then
  684. return false
  685. end
  686. local uname = uname:lower()
  687. local env_from_domain = envfrom[1].domain:lower()
  688. local env_from_addr = envfrom[1].addr:lower()
  689. -- determine newline type
  690. local function newline(task)
  691. local t = task:get_newlines_type()
  692. if t == 'cr' then
  693. return '\r'
  694. elseif t == 'lf' then
  695. return '\n'
  696. end
  697. return '\r\n'
  698. end
  699. -- retrieve footer
  700. local function footer_cb(err_message, code, data, headers)
  701. if err or type(data) ~= 'string' then
  702. rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
  703. else
  704. -- parse json string
  705. local footer = cjson.decode(data)
  706. if not footer then
  707. rspamd_logger.infox(rspamd_config, "parsing domain wide footer for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err)
  708. else
  709. if footer and type(footer) == "table" and (footer.html and footer.html ~= "" or footer.plain and footer.plain ~= "") then
  710. rspamd_logger.infox(rspamd_config, "found domain wide footer for user %s: html=%s, plain=%s, vars=%s", uname, footer.html, footer.plain, footer.vars)
  711. if footer.skip_replies ~= 0 then
  712. in_reply_to = task:get_header_raw('in-reply-to')
  713. if in_reply_to then
  714. rspamd_logger.infox(rspamd_config, "mail is a reply - skip footer")
  715. return
  716. end
  717. end
  718. local envfrom_mime = task:get_from(2)
  719. local from_name = ""
  720. if envfrom_mime and envfrom_mime[1].name then
  721. from_name = envfrom_mime[1].name
  722. elseif envfrom and envfrom[1].name then
  723. from_name = envfrom[1].name
  724. end
  725. -- default replacements
  726. local replacements = {
  727. auth_user = uname,
  728. from_user = envfrom[1].user,
  729. from_name = from_name,
  730. from_addr = envfrom[1].addr,
  731. from_domain = envfrom[1].domain:lower()
  732. }
  733. -- add custom mailbox attributes
  734. if footer.vars and type(footer.vars) == "string" then
  735. local footer_vars = cjson.decode(footer.vars)
  736. if type(footer_vars) == "table" then
  737. for key, value in pairs(footer_vars) do
  738. replacements[key] = value
  739. end
  740. end
  741. end
  742. if footer.html and footer.html ~= "" then
  743. footer.html = lua_util.jinja_template(footer.html, replacements, true)
  744. end
  745. if footer.plain and footer.plain ~= "" then
  746. footer.plain = lua_util.jinja_template(footer.plain, replacements, true)
  747. end
  748. -- add footer
  749. local out = {}
  750. local rewrite = lua_mime.add_text_footer(task, footer.html, footer.plain) or {}
  751. local seen_cte
  752. local newline_s = newline(task)
  753. local function rewrite_ct_cb(name, hdr)
  754. if rewrite.need_rewrite_ct then
  755. if name:lower() == 'content-type' then
  756. -- include boundary if present
  757. local boundary_part = rewrite.new_ct.boundary and
  758. string.format('; boundary="%s"', rewrite.new_ct.boundary) or ''
  759. local nct = string.format('%s: %s/%s; charset=utf-8%s',
  760. 'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part)
  761. out[#out + 1] = nct
  762. -- update Content-Type header (include boundary if present)
  763. task:set_milter_reply({
  764. remove_headers = {['Content-Type'] = 0},
  765. })
  766. task:set_milter_reply({
  767. add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8%s',
  768. rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part)}
  769. })
  770. return
  771. elseif name:lower() == 'content-transfer-encoding' then
  772. out[#out + 1] = string.format('%s: %s',
  773. 'Content-Transfer-Encoding', 'quoted-printable')
  774. -- update Content-Transfer-Encoding header
  775. task:set_milter_reply({
  776. remove_headers = {['Content-Transfer-Encoding'] = 0},
  777. })
  778. task:set_milter_reply({
  779. add_headers = {['Content-Transfer-Encoding'] = 'quoted-printable'}
  780. })
  781. seen_cte = true
  782. return
  783. end
  784. end
  785. out[#out + 1] = hdr.raw:gsub('\r?\n?$', '')
  786. end
  787. task:headers_foreach(rewrite_ct_cb, {full = true})
  788. if not seen_cte and rewrite.need_rewrite_ct then
  789. out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', 'quoted-printable')
  790. end
  791. -- End of headers
  792. out[#out + 1] = newline_s
  793. if rewrite.out then
  794. for _,o in ipairs(rewrite.out) do
  795. out[#out + 1] = o
  796. end
  797. else
  798. out[#out + 1] = task:get_rawbody()
  799. end
  800. local out_parts = {}
  801. for _,o in ipairs(out) do
  802. if type(o) ~= 'table' then
  803. out_parts[#out_parts + 1] = o
  804. out_parts[#out_parts + 1] = newline_s
  805. else
  806. local removePrefix = "--\x0D\x0AContent-Type"
  807. if string.lower(string.sub(tostring(o[1]), 1, string.len(removePrefix))) == string.lower(removePrefix) then
  808. o[1] = string.sub(tostring(o[1]), string.len("--\x0D\x0A") + 1)
  809. end
  810. out_parts[#out_parts + 1] = o[1]
  811. if o[2] then
  812. out_parts[#out_parts + 1] = newline_s
  813. end
  814. end
  815. end
  816. task:set_message(out_parts)
  817. else
  818. rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\")", uname, data)
  819. end
  820. end
  821. end
  822. end
  823. -- fetch footer
  824. rspamd_http.request({
  825. task=task,
  826. url='http://nginx:8081/footer.php',
  827. body='',
  828. callback=footer_cb,
  829. headers={Domain=env_from_domain,Username=uname,From=env_from_addr},
  830. })
  831. return true
  832. end,
  833. priority = 1
  834. })