Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744
  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 rspamd_mempool = require "rspamd_mempool"
  19. local rspamd_url = require "rspamd_url"
  20. local rspamd_text = require "rspamd_text"
  21. local rspamd_util = require "rspamd_util"
  22. local rspamd_dns = require "rspamd_dns"
  23. local fun = require "fun"
  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. -- Creates report XML header
  145. local function report_header(reporting_domain, report_start, report_end, domain_policy)
  146. local report_id = string.format('%s.%d.%d',
  147. reporting_domain, report_start, report_end)
  148. local xml_template = [[
  149. <?xml version="1.0" encoding="UTF-8" ?>
  150. <feedback>
  151. <report_metadata>
  152. <org_name>{= report_settings.org_name | escape_xml =}</org_name>
  153. <email>{= report_settings.email | escape_xml =}</email>
  154. <report_id>{= report_id =}</report_id>
  155. <date_range>
  156. <begin>{= report_start =}</begin>
  157. <end>{= report_end =}</end>
  158. </date_range>
  159. </report_metadata>
  160. <policy_published>
  161. <domain>{= reporting_domain | escape_xml =}</domain>
  162. <adkim>{= domain_policy.adkim | escape_xml =}</adkim>
  163. <aspf>{= domain_policy.aspf | escape_xml =}</aspf>
  164. <p>{= domain_policy.p | escape_xml =}</p>
  165. <sp>{= domain_policy.sp | escape_xml =}</sp>
  166. <pct>{= domain_policy.pct | escape_xml =}</pct>
  167. </policy_published>
  168. ]]
  169. return lua_util.jinja_template(xml_template, {
  170. report_settings = dmarc_settings.reporting,
  171. report_id = report_id,
  172. report_start = report_start,
  173. report_end = report_end,
  174. domain_policy = domain_policy,
  175. reporting_domain = reporting_domain,
  176. }, true, false,
  177. {
  178. escape_xml = escape_xml
  179. })
  180. end
  181. -- Generate xml entry for a preprocessed redis row
  182. local function entry_to_xml(data)
  183. local xml_template = [[<record>
  184. <row>
  185. <source_ip>{= data.ip =}</source_ip>
  186. <count>{= data.count =}</count>
  187. <policy_evaluated>
  188. <disposition>{= data.disposition =}</disposition>
  189. <dkim>{= data.dkim_disposition =}</dkim>
  190. <spf>{= data.spf_disposition =}</spf>
  191. {% if data.override and data.override ~= '' -%}
  192. <reason><type>{= data.override =}</type></reason>
  193. {%- endif %}
  194. </policy_evaluated>
  195. </row>
  196. <identifiers>
  197. <header_from>{= data.header_from =}</header_from>
  198. </identifiers>
  199. <auth_results>
  200. {% if data.dkim_results[1] -%}
  201. {% for d in data.dkim_results -%}
  202. <dkim>
  203. <domain>{= d.domain =}</domain>
  204. <result>{= d.result =}</result>
  205. </dkim>
  206. {%- endfor %}
  207. {%- endif %}
  208. <spf>
  209. <domain>{= data.spf_domain =}</domain>
  210. <result>{= data.spf_result =}</result>
  211. </spf>
  212. </auth_results>
  213. </record>
  214. ]]
  215. return lua_util.jinja_template(xml_template, { data = data }, true,
  216. false, {
  217. escape_xml = escape_xml
  218. })
  219. end
  220. -- Process a report entry stored in Redis splitting it to a lua table
  221. local function process_report_entry(data, score)
  222. local split = lua_util.str_split(data, ',')
  223. local row = {
  224. ip = split[1],
  225. spf_disposition = split[2],
  226. dkim_disposition = split[3],
  227. disposition = split[4],
  228. override = split[5],
  229. header_from = split[6],
  230. dkim_results = {},
  231. spf_domain = split[11],
  232. spf_result = split[12],
  233. count = tonumber(score),
  234. }
  235. -- Process dkim entries
  236. local function dkim_entries_process(dkim_data, result)
  237. if dkim_data and dkim_data ~= '' then
  238. local dkim_elts = lua_util.str_split(dkim_data, '|')
  239. for _, d in ipairs(dkim_elts) do
  240. table.insert(row.dkim_results, { domain = d, result = result })
  241. end
  242. end
  243. end
  244. dkim_entries_process(split[7], 'pass')
  245. dkim_entries_process(split[8], 'fail')
  246. dkim_entries_process(split[9], 'temperror')
  247. dkim_entries_process(split[9], 'permerror')
  248. return row
  249. end
  250. -- Process a single rua entry, validating in DNS if needed
  251. local function process_rua(dmarc_domain, rua)
  252. -- Remove size limitation, as we don't care about them
  253. local addrs = {}
  254. for _, rua_part in fun.map(lua_util.str_trim, lua_util.str_split(rua, ',')) do
  255. local u = rspamd_url.create(pool, rua_part:gsub('!%d+[kmg]?$', ''))
  256. local u2 = rspamd_url.create(pool, dmarc_domain)
  257. if u and (u:get_protocol() or '') == 'mailto' and u:get_user() then
  258. -- Check each address for sanity
  259. if u:get_tld() == u2:get_tld() then
  260. -- Same eSLD - always include
  261. table.insert(addrs, u)
  262. else
  263. -- We need to check authority
  264. local resolve_str = string.format('%s._report._dmarc.%s',
  265. dmarc_domain, u:get_host())
  266. local is_ok, results = rspamd_dns.request({
  267. config = rspamd_config,
  268. session = rspamadm_session,
  269. type = 'txt',
  270. name = resolve_str,
  271. })
  272. if not is_ok then
  273. logger.errx('cannot resolve %s: %s; exclude %s', resolve_str, results, rua_part)
  274. else
  275. local found = false
  276. for _, t in ipairs(results) do
  277. if string.match(t, 'v=DMARC1') then
  278. found = true
  279. break
  280. end
  281. end
  282. if not found then
  283. logger.errx('%s is not authorized to process reports on %s', dmarc_domain, u:get_host())
  284. else
  285. -- All good
  286. table.insert(addrs, u)
  287. end
  288. end
  289. end
  290. else
  291. logger.errx('invalid rua url: "%s""', rua_part)
  292. end
  293. end
  294. if #addrs > 0 then
  295. return addrs
  296. end
  297. return nil
  298. end
  299. -- Validate reporting domain, extracting rua and checking 3rd party report domains
  300. -- This function returns a full dmarc record processed + rua as a list of url objects
  301. local function validate_reporting_domain(reporting_domain)
  302. local is_ok, results = rspamd_dns.request({
  303. config = rspamd_config,
  304. session = rspamadm_session,
  305. type = 'txt',
  306. name = '_dmarc.' .. reporting_domain,
  307. })
  308. if not is_ok or not results then
  309. logger.errx('cannot resolve _dmarc.%s: %s', reporting_domain, results)
  310. return nil
  311. end
  312. for _, r in ipairs(results) do
  313. local processed, rec = dmarc_common.dmarc_check_record(rspamd_config, r, false)
  314. if processed and rec.rua then
  315. -- We need to check or alter rua if needed
  316. local processed_rua = process_rua(reporting_domain, rec.rua)
  317. if processed_rua then
  318. rec = rec.raw_elts
  319. rec.rua = processed_rua
  320. -- Fill defaults in a record to avoid nils in a report
  321. rec['pct'] = rec['pct'] or 100
  322. rec['adkim'] = rec['adkim'] or 'r'
  323. rec['aspf'] = rec['aspf'] or 'r'
  324. rec['p'] = rec['p'] or 'none'
  325. rec['sp'] = rec['sp'] or 'none'
  326. return rec
  327. end
  328. return nil
  329. end
  330. end
  331. return nil
  332. end
  333. -- Returns a list of recipients from a table as a string processing elements if needed
  334. local function rcpt_list(tbl, func)
  335. local res = {}
  336. for _, r in ipairs(tbl) do
  337. if func then
  338. table.insert(res, func(r))
  339. else
  340. table.insert(res, r)
  341. end
  342. end
  343. return table.concat(res, ',')
  344. end
  345. -- Synchronous smtp send function
  346. local function send_reports_by_smtp(opts, reports, finish_cb)
  347. local lua_smtp = require "lua_smtp"
  348. local reports_failed = 0
  349. local reports_sent = 0
  350. local report_settings = dmarc_settings.reporting
  351. local function gen_sendmail_cb(report, args)
  352. return function(ret, err)
  353. -- We modify this from all callbacks
  354. args.nreports = args.nreports - 1
  355. if not ret then
  356. logger.errx("Couldn't send mail for %s: %s", report.reporting_domain, err)
  357. reports_failed = reports_failed + 1
  358. else
  359. reports_sent = reports_sent + 1
  360. lua_util.debugm(N, 'successfully sent a report for %s: %s bytes sent',
  361. report.reporting_domain, #report.message)
  362. end
  363. -- Tail call to the next batch or to the final function
  364. if args.nreports == 0 then
  365. if args.next_start > #reports then
  366. finish_cb(reports_sent, reports_failed)
  367. else
  368. args.cont_func(args.next_start)
  369. end
  370. end
  371. end
  372. end
  373. local function send_data_in_batches(cur_batch)
  374. local nreports = math.min(#reports - cur_batch + 1, opts.batch_size)
  375. local next_start = cur_batch + nreports
  376. lua_util.debugm(N, 'send data for %s domains (from %s to %s)',
  377. nreports, cur_batch, next_start - 1)
  378. -- Shared across all closures
  379. local gen_args = {
  380. cont_func = send_data_in_batches,
  381. nreports = nreports,
  382. next_start = next_start
  383. }
  384. for i = cur_batch, next_start - 1 do
  385. local report = reports[i]
  386. local send_opts = {
  387. ev_base = rspamadm_ev_base,
  388. session = rspamadm_session,
  389. config = rspamd_config,
  390. host = report_settings.smtp,
  391. port = report_settings.smtp_port or 25,
  392. resolver = rspamadm_dns_resolver,
  393. from = report_settings.email,
  394. recipients = report.rcpts,
  395. helo = report_settings.helo or 'rspamd.localhost',
  396. }
  397. lua_smtp.sendmail(send_opts,
  398. report.message,
  399. gen_sendmail_cb(report, gen_args))
  400. end
  401. end
  402. send_data_in_batches(1)
  403. end
  404. local function prepare_report(opts, start_time, end_time, rep_key)
  405. local rua = get_rua(rep_key)
  406. local reporting_domain = get_domain(rep_key)
  407. if not rua then
  408. logger.errx('report %s has no valid rua, skip it', rep_key)
  409. return nil
  410. end
  411. if not reporting_domain then
  412. logger.errx('report %s has no valid reporting_domain, skip it', rep_key)
  413. return nil
  414. end
  415. local ret, results = lua_redis.request(redis_params, redis_attrs,
  416. { 'EXISTS', rep_key })
  417. if not ret or not results or results == 0 then
  418. return nil
  419. end
  420. -- Rename report key to avoid races
  421. if not opts.no_opt then
  422. lua_redis.request(redis_params, redis_attrs,
  423. { 'RENAME', rep_key, rep_key .. '_processing' })
  424. rep_key = rep_key .. '_processing'
  425. end
  426. local dmarc_record = validate_reporting_domain(reporting_domain)
  427. lua_util.debugm(N, 'process reporting domain %s: %s', reporting_domain, dmarc_record)
  428. if not dmarc_record then
  429. if not opts.no_opt then
  430. lua_redis.request(redis_params, redis_attrs,
  431. { 'DEL', rep_key })
  432. end
  433. logger.messagex('Cannot process reports for domain %s; invalid dmarc record', reporting_domain)
  434. return nil
  435. end
  436. -- Get all reports for a domain
  437. ret, results = lua_redis.request(redis_params, redis_attrs,
  438. { 'ZRANGE', rep_key, '0', '-1', 'WITHSCORES' })
  439. local report_entries = {}
  440. table.insert(report_entries,
  441. report_header(reporting_domain, start_time, end_time, dmarc_record))
  442. for i = 1, #results, 2 do
  443. local xml_record = entry_to_xml(process_report_entry(results[i], results[i + 1]))
  444. table.insert(report_entries, xml_record)
  445. end
  446. table.insert(report_entries, '</feedback>')
  447. local xml_to_compress = rspamd_text.fromtable(report_entries)
  448. lua_util.debugm(N, 'got xml: %s', xml_to_compress)
  449. -- Prepare SMTP message
  450. local report_settings = dmarc_settings.reporting
  451. local rcpt_string = rcpt_list(dmarc_record.rua, function(rua_elt)
  452. return string.format('%s@%s', rua_elt:get_user(), rua_elt:get_host())
  453. end)
  454. local bcc_string
  455. if report_settings.bcc_addrs then
  456. bcc_string = rcpt_list(report_settings.bcc_addrs)
  457. end
  458. local uuid = gen_uuid()
  459. local rhead = lua_util.jinja_template(report_template, {
  460. from_name = report_settings.from_name,
  461. from_addr = report_settings.email,
  462. rcpt = rcpt_string,
  463. bcc = bcc_string,
  464. uuid = uuid,
  465. reporting_domain = reporting_domain,
  466. submitter = report_settings.domain,
  467. report_id = string.format('%s.%d.%d', reporting_domain, start_time,
  468. end_time),
  469. report_date = rspamd_util.time_to_string(rspamd_util.get_time()),
  470. message_id = rspamd_util.random_hex(16) .. '@' .. report_settings.msgid_from,
  471. report_start = start_time,
  472. report_end = end_time
  473. }, true,
  474. false, {
  475. escape_xml = escape_xml
  476. })
  477. local rfooter = lua_util.jinja_template(report_footer, {
  478. uuid = uuid,
  479. }, true, false, {
  480. escape_xml = escape_xml
  481. })
  482. local message = rspamd_text.fromtable {
  483. (rhead:gsub("\n", "\r\n")),
  484. rspamd_util.encode_base64(rspamd_util.gzip_compress(xml_to_compress), 73),
  485. rfooter:gsub("\n", "\r\n"),
  486. }
  487. lua_util.debugm(N, 'got final message: %s', message)
  488. if not opts.no_opt then
  489. lua_redis.request(redis_params, redis_attrs,
  490. { 'DEL', rep_key })
  491. end
  492. local report_rcpts = lua_util.str_split(rcpt_string, ',')
  493. if report_settings.bcc_addrs then
  494. for _, b in ipairs(report_settings.bcc_addrs) do
  495. table.insert(report_rcpts, b)
  496. end
  497. end
  498. return {
  499. message = message,
  500. rcpts = report_rcpts,
  501. reporting_domain = reporting_domain
  502. }
  503. end
  504. local function process_report_date(opts, start_time, end_time, date)
  505. local idx_key = redis_prefix(dmarc_settings.reporting.redis_keys.index_prefix, date)
  506. local ret, results = lua_redis.request(redis_params, redis_attrs,
  507. { 'EXISTS', idx_key })
  508. if not ret or not results or results == 0 then
  509. logger.messagex('No reports for %s', date)
  510. return {}
  511. end
  512. -- Rename index key to avoid races
  513. if not opts.no_opt then
  514. lua_redis.request(redis_params, redis_attrs,
  515. { 'RENAME', idx_key, idx_key .. '_processing' })
  516. idx_key = idx_key .. '_processing'
  517. end
  518. ret, results = lua_redis.request(redis_params, redis_attrs,
  519. { 'SMEMBERS', idx_key })
  520. if not ret or not results then
  521. -- Remove bad key
  522. if not opts.no_opt then
  523. lua_redis.request(redis_params, redis_attrs,
  524. { 'DEL', idx_key })
  525. end
  526. logger.messagex('Cannot get reports for %s', date)
  527. return {}
  528. end
  529. local reports = {}
  530. for _, rep in ipairs(results) do
  531. local report = prepare_report(opts, start_time, end_time, rep)
  532. if report then
  533. table.insert(reports, report)
  534. end
  535. end
  536. -- Shuffle reports to make sending more fair
  537. lua_util.shuffle(reports)
  538. -- Remove processed key
  539. if not opts.no_opt then
  540. lua_redis.request(redis_params, redis_attrs,
  541. { 'DEL', idx_key })
  542. end
  543. return reports
  544. end
  545. -- Returns a day before today at 00:00 as unix seconds
  546. local function yesterday_midnight()
  547. local piecewise_time = os.date("*t")
  548. piecewise_time.day = piecewise_time.day - 1 -- Lua allows negative values here
  549. piecewise_time.hour = 0
  550. piecewise_time.sec = 0
  551. piecewise_time.min = 0
  552. return os.time(piecewise_time)
  553. end
  554. -- Returns today time at 00:00 as unix seconds
  555. local function today_midnight()
  556. local piecewise_time = os.date("*t")
  557. piecewise_time.hour = 0
  558. piecewise_time.sec = 0
  559. piecewise_time.min = 0
  560. return os.time(piecewise_time)
  561. end
  562. local function handler(args)
  563. local start_time
  564. -- Preserve start time as report sending might take some time
  565. local start_collection = today_midnight()
  566. local opts = parser:parse(args)
  567. pool = rspamd_mempool.create()
  568. load_config(opts)
  569. rspamd_url.init(rspamd_config:get_tld_path())
  570. if opts.verbose then
  571. lua_util.enable_debug_modules('dmarc', N)
  572. end
  573. dmarc_settings = rspamd_config:get_all_opt('dmarc')
  574. if not dmarc_settings or not dmarc_settings.reporting or not dmarc_settings.reporting.enabled then
  575. logger.errx('dmarc reporting is not enabled, exiting')
  576. os.exit(1)
  577. end
  578. dmarc_settings = lua_util.override_defaults(dmarc_common.default_settings, dmarc_settings)
  579. redis_params = lua_redis.parse_redis_server('dmarc', dmarc_settings)
  580. if not redis_params then
  581. logger.errx('Redis is not configured, exiting')
  582. os.exit(1)
  583. end
  584. for _, e in ipairs({ 'email', 'domain', 'org_name' }) do
  585. if not dmarc_settings.reporting[e] then
  586. logger.errx('Missing required setting: dmarc.reporting.%s', e)
  587. return
  588. end
  589. end
  590. local ret, results = lua_redis.request(redis_params, redis_attrs, {
  591. 'GET', 'rspamd_dmarc_last_collection'
  592. })
  593. if not ret or not tonumber(results) then
  594. start_time = yesterday_midnight()
  595. else
  596. start_time = tonumber(results)
  597. end
  598. lua_util.debugm(N, 'previous last report date is %s', start_time)
  599. if not opts.date or #opts.date == 0 then
  600. opts.date = {}
  601. table.insert(opts.date, os.date('%Y%m%d', yesterday_midnight()))
  602. end
  603. local ndates = 0
  604. local nreports = 0
  605. local all_reports = {}
  606. for _, date in ipairs(opts.date) do
  607. lua_util.debugm(N, 'Process date %s', date)
  608. local reports_for_date = process_report_date(opts, start_time, start_collection, date)
  609. if #reports_for_date > 0 then
  610. ndates = ndates + 1
  611. nreports = nreports + #reports_for_date
  612. for _, r in ipairs(reports_for_date) do
  613. table.insert(all_reports, r)
  614. end
  615. end
  616. end
  617. local function finish_cb(nsuccess, nfail)
  618. if not opts.no_opt then
  619. lua_util.debugm(N, 'set last report date to %s', start_collection)
  620. -- Hack to avoid coroutines + async functions mess: we use async redis call here
  621. redis_attrs.callback = function()
  622. logger.messagex('Reporting collection has finished %s dates processed, %s reports: %s completed, %s failed',
  623. ndates, nreports, nsuccess, nfail)
  624. end
  625. lua_redis.request(redis_params, redis_attrs,
  626. { 'SETEX', 'rspamd_dmarc_last_collection', dmarc_settings.reporting.keys_expire * 2,
  627. tostring(start_collection) })
  628. else
  629. logger.messagex('Reporting collection has finished %s dates processed, %s reports: %s completed, %s failed',
  630. ndates, nreports, nsuccess, nfail)
  631. end
  632. pool:destroy()
  633. end
  634. if not opts.no_opt then
  635. send_reports_by_smtp(opts, all_reports, finish_cb)
  636. else
  637. logger.messagex('Skip sending mails due to -n / --no-opt option')
  638. end
  639. end
  640. return {
  641. name = 'dmarc_report',
  642. aliases = { 'dmarc_reporting' },
  643. handler = handler,
  644. description = parser._description
  645. }