Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

dmarc_report.lua 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739
  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 _, rua_part in ipairs(parts) do
  252. local u = rspamd_url.create(pool, rua_part: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, rua_part)
  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. local send_opts = {
  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. lua_smtp.sendmail(send_opts,
  397. report.message,
  398. gen_sendmail_cb(report, gen_args))
  399. end
  400. end
  401. send_data_in_batches(1)
  402. end
  403. local function prepare_report(opts, start_time, end_time, rep_key)
  404. local rua = get_rua(rep_key)
  405. local reporting_domain = get_domain(rep_key)
  406. if not rua then
  407. logger.errx('report %s has no valid rua, skip it', rep_key)
  408. return nil
  409. end
  410. if not reporting_domain then
  411. logger.errx('report %s has no valid reporting_domain, skip it', rep_key)
  412. return nil
  413. end
  414. local ret, results = lua_redis.request(redis_params, redis_attrs,
  415. { 'EXISTS', rep_key })
  416. if not ret or not results or results == 0 then
  417. return nil
  418. end
  419. -- Rename report key to avoid races
  420. if not opts.no_opt then
  421. lua_redis.request(redis_params, redis_attrs,
  422. { 'RENAME', rep_key, rep_key .. '_processing' })
  423. rep_key = rep_key .. '_processing'
  424. end
  425. local dmarc_record = validate_reporting_domain(reporting_domain)
  426. lua_util.debugm(N, 'process reporting domain %s: %s', reporting_domain, dmarc_record)
  427. if not dmarc_record then
  428. if not opts.no_opt then
  429. lua_redis.request(redis_params, redis_attrs,
  430. { 'DEL', rep_key })
  431. end
  432. logger.messagex('Cannot process reports for domain %s; invalid dmarc record', reporting_domain)
  433. return nil
  434. end
  435. -- Get all reports for a domain
  436. ret, results = lua_redis.request(redis_params, redis_attrs,
  437. { 'ZRANGE', rep_key, '0', '-1', 'WITHSCORES' })
  438. local report_entries = {}
  439. table.insert(report_entries,
  440. report_header(reporting_domain, start_time, end_time, dmarc_record))
  441. for i = 1, #results, 2 do
  442. local xml_record = entry_to_xml(process_report_entry(results[i], results[i + 1]))
  443. table.insert(report_entries, xml_record)
  444. end
  445. table.insert(report_entries, '</feedback>')
  446. local xml_to_compress = rspamd_text.fromtable(report_entries)
  447. lua_util.debugm(N, 'got xml: %s', xml_to_compress)
  448. -- Prepare SMTP message
  449. local report_settings = dmarc_settings.reporting
  450. local rcpt_string = rcpt_list(dmarc_record.rua, function(rua_elt)
  451. return string.format('%s@%s', rua_elt:get_user(), rua_elt:get_host())
  452. end)
  453. local bcc_string
  454. if report_settings.bcc_addrs then
  455. bcc_string = rcpt_list(report_settings.bcc_addrs)
  456. end
  457. local uuid = gen_uuid()
  458. local rhead = lua_util.jinja_template(report_template, {
  459. from_name = report_settings.from_name,
  460. from_addr = report_settings.email,
  461. rcpt = rcpt_string,
  462. bcc = bcc_string,
  463. uuid = uuid,
  464. reporting_domain = reporting_domain,
  465. submitter = report_settings.domain,
  466. report_id = string.format('%s.%d.%d', reporting_domain, start_time,
  467. end_time),
  468. report_date = rspamd_util.time_to_string(rspamd_util.get_time()),
  469. message_id = rspamd_util.random_hex(16) .. '@' .. report_settings.msgid_from,
  470. report_start = start_time,
  471. report_end = end_time
  472. }, true)
  473. local rfooter = lua_util.jinja_template(report_footer, {
  474. uuid = uuid,
  475. }, true)
  476. local message = rspamd_text.fromtable {
  477. (rhead:gsub("\n", "\r\n")),
  478. rspamd_util.encode_base64(rspamd_util.gzip_compress(xml_to_compress), 73),
  479. rfooter:gsub("\n", "\r\n"),
  480. }
  481. lua_util.debugm(N, 'got final message: %s', message)
  482. if not opts.no_opt then
  483. lua_redis.request(redis_params, redis_attrs,
  484. { 'DEL', rep_key })
  485. end
  486. local report_rcpts = lua_util.str_split(rcpt_string, ',')
  487. if report_settings.bcc_addrs then
  488. for _, b in ipairs(report_settings.bcc_addrs) do
  489. table.insert(report_rcpts, b)
  490. end
  491. end
  492. return {
  493. message = message,
  494. rcpts = report_rcpts,
  495. reporting_domain = reporting_domain
  496. }
  497. end
  498. local function process_report_date(opts, start_time, end_time, date)
  499. local idx_key = redis_prefix(dmarc_settings.reporting.redis_keys.index_prefix, date)
  500. local ret, results = lua_redis.request(redis_params, redis_attrs,
  501. { 'EXISTS', idx_key })
  502. if not ret or not results or results == 0 then
  503. logger.messagex('No reports for %s', date)
  504. return {}
  505. end
  506. -- Rename index key to avoid races
  507. if not opts.no_opt then
  508. lua_redis.request(redis_params, redis_attrs,
  509. { 'RENAME', idx_key, idx_key .. '_processing' })
  510. idx_key = idx_key .. '_processing'
  511. end
  512. ret, results = lua_redis.request(redis_params, redis_attrs,
  513. { 'SMEMBERS', idx_key })
  514. if not ret or not results then
  515. -- Remove bad key
  516. if not opts.no_opt then
  517. lua_redis.request(redis_params, redis_attrs,
  518. { 'DEL', idx_key })
  519. end
  520. logger.messagex('Cannot get reports for %s', date)
  521. return {}
  522. end
  523. local reports = {}
  524. for _, rep in ipairs(results) do
  525. local report = prepare_report(opts, start_time, end_time, rep)
  526. if report then
  527. table.insert(reports, report)
  528. end
  529. end
  530. -- Shuffle reports to make sending more fair
  531. lua_util.shuffle(reports)
  532. -- Remove processed key
  533. if not opts.no_opt then
  534. lua_redis.request(redis_params, redis_attrs,
  535. { 'DEL', idx_key })
  536. end
  537. return reports
  538. end
  539. -- Returns a day before today at 00:00 as unix seconds
  540. local function yesterday_midnight()
  541. local piecewise_time = os.date("*t")
  542. piecewise_time.day = piecewise_time.day - 1 -- Lua allows negative values here
  543. piecewise_time.hour = 0
  544. piecewise_time.sec = 0
  545. piecewise_time.min = 0
  546. return os.time(piecewise_time)
  547. end
  548. -- Returns today time at 00:00 as unix seconds
  549. local function today_midnight()
  550. local piecewise_time = os.date("*t")
  551. piecewise_time.hour = 0
  552. piecewise_time.sec = 0
  553. piecewise_time.min = 0
  554. return os.time(piecewise_time)
  555. end
  556. local function handler(args)
  557. local start_time
  558. -- Preserve start time as report sending might take some time
  559. local start_collection = today_midnight()
  560. local opts = parser:parse(args)
  561. pool = rspamd_mempool.create()
  562. load_config(opts)
  563. rspamd_url.init(rspamd_config:get_tld_path())
  564. if opts.verbose then
  565. lua_util.enable_debug_modules('dmarc', N)
  566. end
  567. dmarc_settings = rspamd_config:get_all_opt('dmarc')
  568. if not dmarc_settings or not dmarc_settings.reporting or not dmarc_settings.reporting.enabled then
  569. logger.errx('dmarc reporting is not enabled, exiting')
  570. os.exit(1)
  571. end
  572. dmarc_settings = lua_util.override_defaults(dmarc_common.default_settings, dmarc_settings)
  573. redis_params = lua_redis.parse_redis_server('dmarc', dmarc_settings)
  574. if not redis_params then
  575. logger.errx('Redis is not configured, exiting')
  576. os.exit(1)
  577. end
  578. for _, e in ipairs({ 'email', 'domain', 'org_name' }) do
  579. if not dmarc_settings.reporting[e] then
  580. logger.errx('Missing required setting: dmarc.reporting.%s', e)
  581. return
  582. end
  583. end
  584. local ret, results = lua_redis.request(redis_params, redis_attrs, {
  585. 'GET', 'rspamd_dmarc_last_collection'
  586. })
  587. if not ret or not tonumber(results) then
  588. start_time = yesterday_midnight()
  589. else
  590. start_time = tonumber(results)
  591. end
  592. lua_util.debugm(N, 'previous last report date is %s', start_time)
  593. if not opts.date or #opts.date == 0 then
  594. opts.date = {}
  595. table.insert(opts.date, os.date('%Y%m%d', yesterday_midnight()))
  596. end
  597. local ndates = 0
  598. local nreports = 0
  599. local all_reports = {}
  600. for _, date in ipairs(opts.date) do
  601. lua_util.debugm(N, 'Process date %s', date)
  602. local reports_for_date = process_report_date(opts, start_time, start_collection, date)
  603. if #reports_for_date > 0 then
  604. ndates = ndates + 1
  605. nreports = nreports + #reports_for_date
  606. for _, r in ipairs(reports_for_date) do
  607. table.insert(all_reports, r)
  608. end
  609. end
  610. end
  611. local function finish_cb(nsuccess, nfail)
  612. if not opts.no_opt then
  613. lua_util.debugm(N, 'set last report date to %s', start_collection)
  614. -- Hack to avoid coroutines + async functions mess: we use async redis call here
  615. redis_attrs.callback = function()
  616. logger.messagex('Reporting collection has finished %s dates processed, %s reports: %s completed, %s failed',
  617. ndates, nreports, nsuccess, nfail)
  618. end
  619. lua_redis.request(redis_params, redis_attrs,
  620. { 'SETEX', 'rspamd_dmarc_last_collection', dmarc_settings.reporting.keys_expire * 2,
  621. tostring(start_collection) })
  622. else
  623. logger.messagex('Reporting collection has finished %s dates processed, %s reports: %s completed, %s failed',
  624. ndates, nreports, nsuccess, nfail)
  625. end
  626. pool:destroy()
  627. end
  628. if not opts.no_opt then
  629. send_reports_by_smtp(opts, all_reports, finish_cb)
  630. else
  631. logger.messagex('Skip sending mails due to -n / --no-opt option')
  632. end
  633. end
  634. return {
  635. name = 'dmarc_report',
  636. aliases = { 'dmarc_reporting' },
  637. handler = handler,
  638. description = parser._description
  639. }