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.

dmarc_report.lua 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736
  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. local argparse = require "argparse"
  14. local lua_util = require "lua_util"
  15. local logger = require "rspamd_logger"
  16. local lua_redis = require "lua_redis"
  17. local dmarc_common = require "plugins/dmarc"
  18. local lupa = require "lupa"
  19. local rspamd_mempool = require "rspamd_mempool"
  20. local rspamd_url = require "rspamd_url"
  21. local rspamd_text = require "rspamd_text"
  22. local rspamd_util = require "rspamd_util"
  23. local rspamd_dns = require "rspamd_dns"
  24. local N = 'dmarc_report'
  25. -- Define command line options
  26. local parser = argparse()
  27. :name "rspamadm dmarc_report"
  28. :description "Dmarc reports sending tool"
  29. :help_description_margin(30)
  30. parser:option "-c --config"
  31. :description "Path to config file"
  32. :argname("<cfg>")
  33. :default(rspamd_paths["CONFDIR"] .. "/" .. "rspamd.conf")
  34. parser:flag "-v --verbose"
  35. :description "Enable dmarc specific logging"
  36. parser:flag "-n --no-opt"
  37. :description "Do not reset reporting data/send reports"
  38. parser:argument "date"
  39. :description "Date to process (today by default)"
  40. :argname "<YYYYMMDD>"
  41. :args "*"
  42. parser:option "-b --batch-size"
  43. :description "Send reports in batches up to <batch-size> messages"
  44. :argname "<number>"
  45. :convert(tonumber)
  46. :default "10"
  47. local report_template = [[From: "{= from_name =}" <{= from_addr =}>
  48. To: {= rcpt =}
  49. {%+ if is_string(bcc) %}Bcc: {= bcc =}{%- endif %}
  50. Subject: Report Domain: {= reporting_domain =}
  51. Submitter: {= submitter =}
  52. Report-ID: {= report_id =}
  53. Date: {= report_date =}
  54. MIME-Version: 1.0
  55. Message-ID: <{= message_id =}>
  56. Content-Type: multipart/mixed;
  57. boundary="----=_NextPart_{= uuid =}"
  58. This is a multipart message in MIME format.
  59. ------=_NextPart_{= uuid =}
  60. Content-Type: text/plain; charset="us-ascii"
  61. Content-Transfer-Encoding: 7bit
  62. This is an aggregate report from {= submitter =}.
  63. Report domain: {= reporting_domain =}
  64. Submitter: {= submitter =}
  65. Report ID: {= report_id =}
  66. ------=_NextPart_{= uuid =}
  67. Content-Type: application/gzip
  68. Content-Transfer-Encoding: base64
  69. Content-Disposition: attachment;
  70. filename="{= submitter =}!{= reporting_domain =}!{= report_start =}!{= report_end =}.xml.gz"
  71. ]]
  72. local report_footer = [[
  73. ------=_NextPart_{= uuid =}--]]
  74. local dmarc_settings = {}
  75. local redis_params
  76. local redis_attrs = {
  77. config = rspamd_config,
  78. ev_base = rspamadm_ev_base,
  79. session = rspamadm_session,
  80. log_obj = rspamd_config,
  81. resolver = rspamadm_dns_resolver,
  82. }
  83. local pool
  84. local function load_config(opts)
  85. local _r,err = rspamd_config:load_ucl(opts['config'])
  86. if not _r then
  87. logger.errx('cannot parse %s: %s', opts['config'], err)
  88. os.exit(1)
  89. end
  90. _r,err = rspamd_config:parse_rcl({'logging', 'worker'})
  91. if not _r then
  92. logger.errx('cannot process %s: %s', opts['config'], err)
  93. os.exit(1)
  94. end
  95. end
  96. -- Concat elements using redis_keys.join_char
  97. local function redis_prefix(...)
  98. return table.concat({...}, dmarc_settings.reporting.redis_keys.join_char)
  99. end
  100. local function get_rua(rep_key)
  101. local parts = lua_util.str_split(rep_key, dmarc_settings.reporting.redis_keys.join_char)
  102. if #parts >= 3 then
  103. return parts[3]
  104. end
  105. return nil
  106. end
  107. local function get_domain(rep_key)
  108. local parts = lua_util.str_split(rep_key, dmarc_settings.reporting.redis_keys.join_char)
  109. if #parts >= 3 then
  110. return parts[2]
  111. end
  112. return nil
  113. end
  114. local function gen_uuid()
  115. local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
  116. return string.gsub(template, '[xy]', function (c)
  117. local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb)
  118. return string.format('%x', v)
  119. end)
  120. end
  121. local function gen_xml_grammar()
  122. local lpeg = require 'lpeg'
  123. local lt = lpeg.P('<') / '&lt;'
  124. local gt = lpeg.P('>') / '&gt;'
  125. local amp = lpeg.P('&') / '&amp;'
  126. local quot = lpeg.P('"') / '&quot;'
  127. local apos = lpeg.P("'") / '&apos;'
  128. local special = lt + gt + amp + quot + apos
  129. local grammar = lpeg.Cs((special + 1)^0)
  130. return grammar
  131. end
  132. local xml_grammar = gen_xml_grammar()
  133. local function escape_xml(input)
  134. if type(input) == 'string' or type(input) == 'userdata' then
  135. return xml_grammar:match(input)
  136. else
  137. input = tostring(input)
  138. if input then
  139. return xml_grammar:match(input)
  140. end
  141. end
  142. return ''
  143. end
  144. -- Enable xml escaping in lupa templates
  145. lupa.filters.escape_xml = escape_xml
  146. -- Creates report XML header
  147. local function report_header(reporting_domain, report_start, report_end, domain_policy)
  148. local report_id = string.format('%s.%d.%d',
  149. reporting_domain, report_start, report_end)
  150. local xml_template = [[
  151. <?xml version="1.0" encoding="UTF-8" ?>
  152. <feedback>
  153. <report_metadata>
  154. <org_name>{= report_settings.org_name | escape_xml =}</org_name>
  155. <email>{= report_settings.email | escape_xml =}</email>
  156. <report_id>{= report_id =}</report_id>
  157. <date_range>
  158. <begin>{= report_start =}</begin>
  159. <end>{= report_end =}</end>
  160. </date_range>
  161. </report_metadata>
  162. <policy_published>
  163. <domain>{= reporting_domain | escape_xml =}</domain>
  164. <adkim>{= domain_policy.adkim | escape_xml =}</adkim>
  165. <aspf>{= domain_policy.aspf | escape_xml =}</aspf>
  166. <p>{= domain_policy.p | escape_xml =}</p>
  167. <sp>{= domain_policy.sp | escape_xml =}</sp>
  168. <pct>{= domain_policy.pct | escape_xml =}</pct>
  169. </policy_published>
  170. ]]
  171. return lua_util.jinja_template(xml_template, {
  172. report_settings = dmarc_settings.reporting,
  173. report_id = report_id,
  174. report_start = report_start,
  175. report_end = report_end,
  176. domain_policy = domain_policy,
  177. reporting_domain = reporting_domain,
  178. }, true)
  179. end
  180. -- Generate xml entry for a preprocessed redis row
  181. local function entry_to_xml(data)
  182. local xml_template = [[<record>
  183. <row>
  184. <source_ip>{= data.ip =}</source_ip>
  185. <count>{= data.count =}</count>
  186. <policy_evaluated>
  187. <disposition>{= data.disposition =}</disposition>
  188. <dkim>{= data.dkim_disposition =}</dkim>
  189. <spf>{= data.spf_disposition =}</spf>
  190. {% if data.override and data.override ~= '' -%}
  191. <reason><type>{= data.override =}</type></reason>
  192. {%- endif %}
  193. </policy_evaluated>
  194. </row>
  195. <identifiers>
  196. <header_from>{= data.header_from =}</header_from>
  197. </identifiers>
  198. <auth_results>
  199. {% if data.dkim_results[1] -%}
  200. {% for d in data.dkim_results -%}
  201. <dkim>
  202. <domain>{= d.domain =}</domain>
  203. <result>{= d.result =}</result>
  204. </dkim>
  205. {%- endfor %}
  206. {%- endif %}
  207. <spf>
  208. <domain>{= data.spf_domain =}</domain>
  209. <result>{= data.spf_result =}</result>
  210. </spf>
  211. </auth_results>
  212. </record>
  213. ]]
  214. return lua_util.jinja_template(xml_template, {data = data}, true)
  215. end
  216. -- Process a report entry stored in Redis splitting it to a lua table
  217. local function process_report_entry(data, score)
  218. local split = lua_util.str_split(data, ',')
  219. local row = {
  220. ip = split[1],
  221. spf_disposition = split[2],
  222. dkim_disposition = split[3],
  223. disposition = split[4],
  224. override = split[5],
  225. header_from = split[6],
  226. dkim_results = {},
  227. spf_domain = split[11],
  228. spf_result = split[12],
  229. count = tonumber(score),
  230. }
  231. -- Process dkim entries
  232. local function dkim_entries_process(dkim_data, result)
  233. if dkim_data and dkim_data ~= '' then
  234. local dkim_elts = lua_util.str_split(dkim_data, '|')
  235. for _, d in ipairs(dkim_elts) do
  236. table.insert(row.dkim_results, {domain = d, result = result})
  237. end
  238. end
  239. end
  240. dkim_entries_process(split[7], 'pass')
  241. dkim_entries_process(split[8], 'fail')
  242. dkim_entries_process(split[9], 'temperror')
  243. dkim_entries_process(split[9], 'permerror')
  244. return row
  245. end
  246. -- Process a single rua entry, validating in DNS if needed
  247. local function process_rua(dmarc_domain, rua)
  248. local parts = lua_util.str_split(rua, ',')
  249. -- Remove size limitation, as we don't care about them
  250. local addrs = {}
  251. for _,a in ipairs(parts) do
  252. local u = rspamd_url.create(pool, a:gsub('!%d+[kmg]?$', ''))
  253. if u and (u:get_protocol() or '') == 'mailto' and u:get_user() then
  254. -- Check each address for sanity
  255. if dmarc_domain == u:get_tld() or dmarc_domain == u:get_host() then
  256. -- Same domain - always include
  257. table.insert(addrs, u)
  258. else
  259. -- We need to check authority
  260. local resolve_str = string.format('%s._report._dmarc.%s',
  261. dmarc_domain, u:get_host())
  262. local is_ok, results = rspamd_dns.request({
  263. config = rspamd_config,
  264. session = rspamadm_session,
  265. type = 'txt',
  266. name = resolve_str,
  267. })
  268. if not is_ok then
  269. logger.errx('cannot resolve %s: %s; exclude %s', resolve_str, results, a)
  270. else
  271. local found = false
  272. for _,t in ipairs(results) do
  273. if string.match(t, 'v=DMARC1') then
  274. found = true
  275. break
  276. end
  277. end
  278. if not found then
  279. logger.errx('%s is not authorized to process reports on %s', dmarc_domain, u:get_host())
  280. else
  281. -- All good
  282. table.insert(addrs, u)
  283. end
  284. end
  285. end
  286. else
  287. logger.errx('invalid rua url: "%s""', tostring(u or 'null'))
  288. end
  289. end
  290. if #addrs > 0 then
  291. return addrs
  292. end
  293. return nil
  294. end
  295. -- Validate reporting domain, extracting rua and checking 3rd party report domains
  296. -- This function returns a full dmarc record processed + rua as a list of url objects
  297. local function validate_reporting_domain(reporting_domain)
  298. -- Now check the domain policy
  299. -- DMARC domain is a esld for the reporting domain
  300. local dmarc_domain = rspamd_util.get_tld(reporting_domain)
  301. local is_ok, results = rspamd_dns.request({
  302. config = rspamd_config,
  303. session = rspamadm_session,
  304. type = 'txt',
  305. name = '_dmarc.' .. dmarc_domain ,
  306. })
  307. if not is_ok or not results then
  308. logger.errx('cannot resolve _dmarc.%s: %s', dmarc_domain, results)
  309. return nil
  310. end
  311. for _,r in ipairs(results) do
  312. local processed,rec = dmarc_common.dmarc_check_record(rspamd_config, r, false)
  313. if processed and rec.rua then
  314. -- We need to check or alter rua if needed
  315. local processed_rua = process_rua(dmarc_domain, rec.rua)
  316. if processed_rua then
  317. rec = rec.raw_elts
  318. rec.rua = processed_rua
  319. -- Fill defaults in a record to avoid nils in a report
  320. rec['pct'] = rec['pct'] or 100
  321. rec['adkim'] = rec['adkim'] or 'r'
  322. rec['aspf'] = rec['aspf'] or 'r'
  323. rec['p'] = rec['p'] or 'none'
  324. rec['sp'] = rec['sp'] or 'none'
  325. return rec
  326. end
  327. return nil
  328. end
  329. end
  330. return nil
  331. end
  332. -- Returns a list of recipients from a table as a string processing elements if needed
  333. local function rcpt_list(tbl, func)
  334. local res = {}
  335. for _,r in ipairs(tbl) do
  336. if func then
  337. table.insert(res, func(r))
  338. else
  339. table.insert(res, r)
  340. end
  341. end
  342. return table.concat(res, ',')
  343. end
  344. -- Synchronous smtp send function
  345. local function send_reports_by_smtp(opts, reports, finish_cb)
  346. local lua_smtp = require "lua_smtp"
  347. local reports_failed = 0
  348. local reports_sent = 0
  349. local report_settings = dmarc_settings.reporting
  350. local function gen_sendmail_cb(report, args)
  351. return function(ret, err)
  352. -- We modify this from all callbacks
  353. args.nreports = args.nreports - 1
  354. if not ret then
  355. logger.errx("Couldn't send mail for %s: %s", report.reporting_domain, err)
  356. reports_failed = reports_failed + 1
  357. else
  358. reports_sent = reports_sent + 1
  359. lua_util.debugm(N, 'successfully sent a report for %s: %s bytes sent',
  360. report.reporting_domain, #report.message)
  361. end
  362. -- Tail call to the next batch or to the final function
  363. if args.nreports == 0 then
  364. if args.next_start > #reports then
  365. finish_cb(reports_sent, reports_failed)
  366. else
  367. args.cont_func(args.next_start)
  368. end
  369. end
  370. end
  371. end
  372. local function send_data_in_batches(cur_batch)
  373. local nreports = math.min(#reports - cur_batch + 1, opts.batch_size)
  374. local next_start = cur_batch + nreports
  375. lua_util.debugm(N, 'send data for %s domains (from %s to %s)',
  376. nreports, cur_batch, next_start-1)
  377. -- Shared across all closures
  378. local gen_args = {
  379. cont_func = send_data_in_batches,
  380. nreports = nreports,
  381. next_start = next_start
  382. }
  383. for i=cur_batch,next_start-1 do
  384. local report = reports[i]
  385. lua_smtp.sendmail({
  386. ev_base = rspamadm_ev_base,
  387. session = rspamadm_session,
  388. config = rspamd_config,
  389. host = report_settings.smtp,
  390. port = report_settings.smtp_port or 25,
  391. resolver = rspamadm_dns_resolver,
  392. from = report_settings.email,
  393. recipients = report.rcpts,
  394. helo = report_settings.helo or 'rspamd.localhost',
  395. },
  396. report.message,
  397. gen_sendmail_cb(report, gen_args))
  398. end
  399. end
  400. send_data_in_batches(1)
  401. end
  402. local function prepare_report(opts, start_time, end_time, rep_key)
  403. local rua = get_rua(rep_key)
  404. local reporting_domain = get_domain(rep_key)
  405. if not rua then
  406. logger.errx('report %s has no valid rua, skip it', rep_key)
  407. return nil
  408. end
  409. if not reporting_domain then
  410. logger.errx('report %s has no valid reporting_domain, skip it', rep_key)
  411. return nil
  412. end
  413. local ret, results = lua_redis.request(redis_params, redis_attrs,
  414. {'EXISTS', rep_key})
  415. if not ret or not results or results == 0 then
  416. return nil
  417. end
  418. -- Rename report key to avoid races
  419. if not opts.no_opt then
  420. lua_redis.request(redis_params, redis_attrs,
  421. {'RENAME', rep_key, rep_key .. '_processing'})
  422. rep_key = rep_key .. '_processing'
  423. end
  424. local dmarc_record = validate_reporting_domain(reporting_domain)
  425. lua_util.debugm(N, 'process reporting domain %s: %s', reporting_domain, dmarc_record)
  426. if not dmarc_record then
  427. if not opts.no_opt then
  428. lua_redis.request(redis_params, redis_attrs,
  429. {'DEL', rep_key})
  430. end
  431. logger.messagex('Cannot process reports for domain %s; invalid dmarc record', reporting_domain)
  432. return nil
  433. end
  434. -- Get all reports for a domain
  435. ret, results = lua_redis.request(redis_params, redis_attrs,
  436. {'ZRANGE', rep_key, '0', '-1', 'WITHSCORES'})
  437. local report_entries = {}
  438. table.insert(report_entries,
  439. report_header(reporting_domain, start_time, end_time, dmarc_record))
  440. for i=1,#results,2 do
  441. local xml_record = entry_to_xml(process_report_entry(results[i], results[i + 1]))
  442. table.insert(report_entries, xml_record)
  443. end
  444. table.insert(report_entries, '</feedback>')
  445. local xml_to_compress = rspamd_text.fromtable(report_entries)
  446. lua_util.debugm(N, 'got xml: %s', xml_to_compress)
  447. -- Prepare SMTP message
  448. local report_settings = dmarc_settings.reporting
  449. local rcpt_string = rcpt_list(dmarc_record.rua, function(rua_elt)
  450. return string.format('%s@%s', rua_elt:get_user(), rua_elt:get_host())
  451. end)
  452. local bcc_string
  453. if report_settings.bcc_addrs then
  454. bcc_string = rcpt_list(report_settings.bcc_addrs)
  455. end
  456. local uuid = gen_uuid()
  457. local rhead = lua_util.jinja_template(report_template, {
  458. from_name = report_settings.from_name,
  459. from_addr = report_settings.email,
  460. rcpt = rcpt_string,
  461. bcc = bcc_string,
  462. uuid = uuid,
  463. reporting_domain = reporting_domain,
  464. submitter = report_settings.domain,
  465. report_id = string.format('%s.%d.%d', reporting_domain, start_time,
  466. end_time),
  467. report_date = rspamd_util.time_to_string(rspamd_util.get_time()),
  468. message_id = rspamd_util.random_hex(16) .. '@' .. report_settings.msgid_from,
  469. report_start = start_time,
  470. report_end = end_time
  471. }, true)
  472. local rfooter = lua_util.jinja_template(report_footer, {
  473. uuid = uuid,
  474. }, true)
  475. local message = rspamd_text.fromtable{
  476. (rhead:gsub("\n", "\r\n")),
  477. rspamd_util.encode_base64(rspamd_util.gzip_compress(xml_to_compress), 73),
  478. rfooter:gsub("\n", "\r\n"),
  479. }
  480. lua_util.debugm(N, 'got final message: %s', message)
  481. if not opts.no_opt then
  482. lua_redis.request(redis_params, redis_attrs,
  483. {'DEL', rep_key})
  484. end
  485. local report_rcpts = lua_util.str_split(rcpt_string, ',')
  486. if report_settings.bcc_addrs then
  487. for _,b in ipairs(report_settings.bcc_addrs) do
  488. table.insert(report_rcpts, b)
  489. end
  490. end
  491. return {
  492. message = message,
  493. rcpts = report_rcpts,
  494. reporting_domain = reporting_domain
  495. }
  496. end
  497. local function process_report_date(opts, start_time, end_time, date)
  498. local idx_key = redis_prefix(dmarc_settings.reporting.redis_keys.index_prefix, date)
  499. local ret, results = lua_redis.request(redis_params, redis_attrs,
  500. {'EXISTS', idx_key})
  501. if not ret or not results or results == 0 then
  502. logger.messagex('No reports for %s', date)
  503. return {}
  504. end
  505. -- Rename index key to avoid races
  506. if not opts.no_opt then
  507. lua_redis.request(redis_params, redis_attrs,
  508. {'RENAME', idx_key, idx_key .. '_processing'})
  509. idx_key = idx_key .. '_processing'
  510. end
  511. ret, results = lua_redis.request(redis_params, redis_attrs,
  512. {'SMEMBERS', idx_key})
  513. if not ret or not results then
  514. -- Remove bad key
  515. if not opts.no_opt then
  516. lua_redis.request(redis_params, redis_attrs,
  517. {'DEL', idx_key})
  518. end
  519. logger.messagex('Cannot get reports for %s', date)
  520. return {}
  521. end
  522. local reports = {}
  523. for _,rep in ipairs(results) do
  524. local report = prepare_report(opts, start_time, end_time, rep)
  525. if report then
  526. table.insert(reports, report)
  527. end
  528. end
  529. -- Shuffle reports to make sending more fair
  530. lua_util.shuffle(reports)
  531. -- Remove processed key
  532. if not opts.no_opt then
  533. lua_redis.request(redis_params, redis_attrs,
  534. {'DEL', idx_key})
  535. end
  536. return reports
  537. end
  538. -- Returns a day before today at 00:00 as unix seconds
  539. local function yesterday_midnight()
  540. local piecewise_time = os.date("*t")
  541. piecewise_time.day = piecewise_time.day - 1 -- Lua allows negative values here
  542. piecewise_time.hour = 0
  543. piecewise_time.sec = 0
  544. piecewise_time.min = 0
  545. return os.time(piecewise_time)
  546. end
  547. -- Returns today time at 00:00 as unix seconds
  548. local function today_midnight()
  549. local piecewise_time = os.date("*t")
  550. piecewise_time.hour = 0
  551. piecewise_time.sec = 0
  552. piecewise_time.min = 0
  553. return os.time(piecewise_time)
  554. end
  555. local function handler(args)
  556. local start_time
  557. -- Preserve start time as report sending might take some time
  558. local start_collection = today_midnight()
  559. local opts = parser:parse(args)
  560. pool = rspamd_mempool.create()
  561. load_config(opts)
  562. rspamd_url.init(rspamd_config:get_tld_path())
  563. if opts.verbose then
  564. lua_util.enable_debug_modules('dmarc', N)
  565. end
  566. dmarc_settings = rspamd_config:get_all_opt('dmarc')
  567. if not dmarc_settings or not dmarc_settings.reporting or not dmarc_settings.reporting.enabled then
  568. logger.errx('dmarc reporting is not enabled, exiting')
  569. os.exit(1)
  570. end
  571. dmarc_settings = lua_util.override_defaults(dmarc_common.default_settings, dmarc_settings)
  572. redis_params = lua_redis.parse_redis_server('dmarc', dmarc_settings)
  573. if not redis_params then
  574. logger.errx('Redis is not configured, exiting')
  575. os.exit(1)
  576. end
  577. for _, e in ipairs({'email', 'domain', 'org_name'}) do
  578. if not dmarc_settings.reporting[e] then
  579. logger.errx('Missing required setting: dmarc.reporting.%s', e)
  580. return
  581. end
  582. end
  583. local ret,results = lua_redis.request(redis_params, redis_attrs, {
  584. 'GET', 'rspamd_dmarc_last_collection'
  585. })
  586. if not ret or not tonumber(results) then
  587. start_time = yesterday_midnight()
  588. else
  589. start_time = tonumber(results)
  590. end
  591. lua_util.debugm(N, 'previous last report date is %s', start_time)
  592. if not opts.date or #opts.date == 0 then
  593. opts.date = {}
  594. table.insert(opts.date, os.date('%Y%m%d', yesterday_midnight()))
  595. end
  596. local ndates = 0
  597. local nreports = 0
  598. local all_reports = {}
  599. for _,date in ipairs(opts.date) do
  600. lua_util.debugm(N, 'Process date %s', date)
  601. local reports_for_date = process_report_date(opts, start_time, start_collection, date)
  602. if #reports_for_date > 0 then
  603. ndates = ndates + 1
  604. nreports = nreports + #reports_for_date
  605. for _,r in ipairs(reports_for_date) do
  606. table.insert(all_reports, r)
  607. end
  608. end
  609. end
  610. local function finish_cb(nsuccess, nfail)
  611. if not opts.no_opt then
  612. lua_util.debugm(N, 'set last report date to %s', start_collection)
  613. -- Hack to avoid coroutines + async functions mess: we use async redis call here
  614. redis_attrs.callback = function()
  615. logger.messagex('Reporting collection has finished %s dates processed, %s reports: %s completed, %s failed',
  616. ndates, nreports, nsuccess, nfail)
  617. end
  618. lua_redis.request(redis_params, redis_attrs,
  619. {'SETEX', 'rspamd_dmarc_last_collection', dmarc_settings.reporting.keys_expire * 2,
  620. tostring(start_collection)})
  621. else
  622. logger.messagex('Reporting collection has finished %s dates processed, %s reports: %s completed, %s failed',
  623. ndates, nreports, nsuccess, nfail)
  624. end
  625. pool:destroy()
  626. end
  627. send_reports_by_smtp(opts, all_reports, finish_cb)
  628. end
  629. return {
  630. name = 'dmarc_report',
  631. aliases = {'dmarc_reporting'},
  632. handler = handler,
  633. description = parser._description
  634. }