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.lua 47KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443
  1. --[[
  2. Copyright (c) 2011-2016, Vsevolod Stakhov <vsevolod@highsecure.ru>
  3. Copyright (c) 2015-2016, Andrew Lewis <nerf@judo.za.org>
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. ]]--
  14. -- Dmarc policy filter
  15. local rspamd_logger = require "rspamd_logger"
  16. local mempool = require "rspamd_mempool"
  17. local rspamd_url = require "rspamd_url"
  18. local rspamd_util = require "rspamd_util"
  19. local rspamd_redis = require "lua_redis"
  20. local lua_util = require "lua_util"
  21. local auth_and_local_conf
  22. if confighelp then
  23. return
  24. end
  25. local N = 'dmarc'
  26. local no_sampling_domains
  27. local no_reporting_domains
  28. local statefile = string.format('%s/%s', rspamd_paths['DBDIR'], 'dmarc_reports_last_sent')
  29. local VAR_NAME = 'dmarc_reports_last_sent'
  30. local INTERVAL = 86400
  31. local pool
  32. local report_settings = {
  33. helo = 'rspamd',
  34. hscan_count = 1000,
  35. smtp = '127.0.0.1',
  36. smtp_port = 25,
  37. retries = 2,
  38. from_name = 'Rspamd',
  39. msgid_from = 'rspamd',
  40. }
  41. local report_template = [[From: "{= from_name =}" <{= from_addr =}>
  42. To: {= rcpt =}
  43. {%+ if is_string(bcc) %}Bcc: {= bcc =}{%- endif %}
  44. Subject: Report Domain: {= reporting_domain =}
  45. Submitter: {= submitter =}
  46. Report-ID: {= report_id =}
  47. Date: {= report_date =}
  48. MIME-Version: 1.0
  49. Message-ID: <{= message_id =}>
  50. Content-Type: multipart/mixed;
  51. boundary="----=_NextPart_000_024E_01CC9B0A.AFE54C00"
  52. This is a multipart message in MIME format.
  53. ------=_NextPart_000_024E_01CC9B0A.AFE54C00
  54. Content-Type: text/plain; charset="us-ascii"
  55. Content-Transfer-Encoding: 7bit
  56. This is an aggregate report from {= submitter =}.
  57. Report domain: {= reporting_domain =}
  58. Submitter: {= submitter =}
  59. Report ID: {= report_id =}
  60. ------=_NextPart_000_024E_01CC9B0A.AFE54C00
  61. Content-Type: application/gzip
  62. Content-Transfer-Encoding: base64
  63. Content-Disposition: attachment;
  64. filename="{= submitter =}!{= reporting_domain =}!{= report_start =}!{= report_end =}.xml.gz"
  65. ]]
  66. local report_footer = [[
  67. ------=_NextPart_000_024E_01CC9B0A.AFE54C00--]]
  68. local symbols = {
  69. spf_allow_symbol = 'R_SPF_ALLOW',
  70. spf_deny_symbol = 'R_SPF_FAIL',
  71. spf_softfail_symbol = 'R_SPF_SOFTFAIL',
  72. spf_neutral_symbol = 'R_SPF_NEUTRAL',
  73. spf_tempfail_symbol = 'R_SPF_DNSFAIL',
  74. spf_permfail_symbol = 'R_SPF_PERMFAIL',
  75. spf_na_symbol = 'R_SPF_NA',
  76. dkim_allow_symbol = 'R_DKIM_ALLOW',
  77. dkim_deny_symbol = 'R_DKIM_REJECT',
  78. dkim_tempfail_symbol = 'R_DKIM_TEMPFAIL',
  79. dkim_na_symbol = 'R_DKIM_NA',
  80. dkim_permfail_symbol = 'R_DKIM_PERMFAIL',
  81. }
  82. local dmarc_symbols = {
  83. allow = 'DMARC_POLICY_ALLOW',
  84. badpolicy = 'DMARC_BAD_POLICY',
  85. dnsfail = 'DMARC_DNSFAIL',
  86. na = 'DMARC_NA',
  87. reject = 'DMARC_POLICY_REJECT',
  88. softfail = 'DMARC_POLICY_SOFTFAIL',
  89. quarantine = 'DMARC_POLICY_QUARANTINE',
  90. }
  91. local redis_keys = {
  92. index_prefix = 'dmarc_idx',
  93. report_prefix = 'dmarc',
  94. join_char = ';',
  95. }
  96. local function gen_xml_grammar()
  97. local lpeg = require 'lpeg'
  98. local lt = lpeg.P('<') / '&lt;'
  99. local gt = lpeg.P('>') / '&gt;'
  100. local amp = lpeg.P('&') / '&amp;'
  101. local quot = lpeg.P('"') / '&quot;'
  102. local apos = lpeg.P("'") / '&apos;'
  103. local special = lt + gt + amp + quot + apos
  104. local grammar = lpeg.Cs((special + 1)^0)
  105. return grammar
  106. end
  107. local xml_grammar = gen_xml_grammar()
  108. local function escape_xml(input)
  109. if type(input) == 'string' or type(input) == 'userdata' then
  110. return xml_grammar:match(input)
  111. else
  112. input = tostring(input)
  113. if input then
  114. return xml_grammar:match(input)
  115. end
  116. end
  117. return ''
  118. end
  119. -- Default port for redis upstreams
  120. local redis_params = nil
  121. -- 2 days
  122. local dmarc_reporting = false
  123. local dmarc_actions = {}
  124. local E = {}
  125. local take_report_id
  126. local take_report_script = [[
  127. local index_key = KEYS[1]
  128. local report_key = KEYS[2]
  129. local dmarc_domain = ARGV[1]
  130. local report = ARGV[2]
  131. redis.call('SADD', index_key, report_key)
  132. redis.call('EXPIRE', index_key, 172800)
  133. redis.call('HINCRBY', report_key, report, 1)
  134. redis.call('EXPIRE', report_key, 172800)
  135. ]]
  136. -- return the timezone offset in seconds, as it was on the time given by ts
  137. -- Eric Feliksik
  138. local function get_timezone_offset(ts)
  139. local utcdate = os.date("!*t", ts)
  140. local localdate = os.date("*t", ts)
  141. localdate.isdst = false -- this is the trick
  142. return os.difftime(os.time(localdate), os.time(utcdate))
  143. end
  144. local tz_offset = get_timezone_offset(os.time())
  145. local function gen_dmarc_grammar()
  146. local lpeg = require "lpeg"
  147. lpeg.locale(lpeg)
  148. local space = lpeg.space^0
  149. local name = lpeg.C(lpeg.alpha^1) * space
  150. local sep = lpeg.S("\\;") * space
  151. local value = lpeg.C(lpeg.P(lpeg.graph - sep)^1)
  152. local pair = lpeg.Cg(name * "=" * space * value) * sep^-1
  153. local list = lpeg.Cf(lpeg.Ct("") * pair^0, rawset)
  154. local version = lpeg.P("v") * space * lpeg.P("=") * space * lpeg.P("DMARC1")
  155. local record = version * space * sep * list
  156. return record
  157. end
  158. local dmarc_grammar = gen_dmarc_grammar()
  159. local function dmarc_key_value_case(elts)
  160. if type(elts) ~= "table" then
  161. return elts
  162. end
  163. local result = {}
  164. for k, v in pairs(elts) do
  165. k = k:lower()
  166. if k ~= "v" then
  167. v = v:lower()
  168. end
  169. result[k] = v
  170. end
  171. return result
  172. end
  173. local function dmarc_report(task, spf_ok, dkim_ok, disposition,
  174. sampled_out, hfromdom, spfdom, dres, spf_result)
  175. local ip = task:get_from_ip()
  176. if ip and not ip:is_valid() then
  177. return nil
  178. end
  179. local rspamd_lua_utils = require "lua_util"
  180. if rspamd_lua_utils.is_rspamc_or_controller(task) then return end
  181. local dkim_pass = table.concat(dres.pass or E, '|')
  182. local dkim_fail = table.concat(dres.fail or E, '|')
  183. local dkim_temperror = table.concat(dres.temperror or E, '|')
  184. local dkim_permerror = table.concat(dres.permerror or E, '|')
  185. local disposition_to_return = if disposition == "softfail" then "none" else disposition
  186. local res = table.concat({
  187. ip:to_string(), spf_ok, dkim_ok,
  188. disposition_to_return, (sampled_out and 'sampled_out' or ''), hfromdom,
  189. dkim_pass, dkim_fail, dkim_temperror, dkim_permerror, spfdom, spf_result}, ',')
  190. return res
  191. end
  192. local function maybe_force_action(task, disposition)
  193. if disposition then
  194. local force_action = dmarc_actions[disposition]
  195. if force_action then
  196. -- Don't do anything if pre-result has been already set
  197. if task:has_pre_result() then return end
  198. task:set_pre_result(force_action, 'Action set by DMARC', N, nil, nil, 'least')
  199. end
  200. end
  201. end
  202. --[[
  203. -- Used to check dmarc record, check elements and produce dmarc policy processed
  204. -- result.
  205. -- Returns:
  206. -- false,false - record is garbadge
  207. -- false,error_message - record is invalid
  208. -- true,policy_table - record is valid and parsed
  209. ]]
  210. local function dmarc_check_record(task, record, is_tld)
  211. local failed_policy
  212. local result = {
  213. dmarc_policy = 'none'
  214. }
  215. local elts = dmarc_grammar:match(record)
  216. lua_util.debugm(N, task, "got DMARC record: %s, tld_flag=%s, processed=%s",
  217. record, is_tld, elts)
  218. if elts then
  219. elts = dmarc_key_value_case(elts)
  220. local dkim_pol = elts['adkim']
  221. if dkim_pol then
  222. if dkim_pol == 's' then
  223. result.strict_dkim = true
  224. elseif dkim_pol ~= 'r' then
  225. failed_policy = 'adkim tag has invalid value: ' .. dkim_pol
  226. return false,failed_policy
  227. end
  228. end
  229. local spf_pol = elts['aspf']
  230. if spf_pol then
  231. if spf_pol == 's' then
  232. result.strict_spf = true
  233. elseif spf_pol ~= 'r' then
  234. failed_policy = 'aspf tag has invalid value: ' .. spf_pol
  235. return false,failed_policy
  236. end
  237. end
  238. local policy = elts['p']
  239. if policy then
  240. if (policy == 'reject') then
  241. result.dmarc_policy = 'reject'
  242. elseif (policy == 'quarantine') then
  243. result.dmarc_policy = 'quarantine'
  244. elseif (policy ~= 'none') then
  245. failed_policy = 'p tag has invalid value: ' .. policy
  246. return false,failed_policy
  247. end
  248. end
  249. -- Adjust policy if we are in tld mode
  250. local subdomain_policy = elts['sp']
  251. if elts['sp'] and is_tld then
  252. result.subdomain_policy = elts['sp']
  253. if (subdomain_policy == 'reject') then
  254. result.dmarc_policy = 'reject'
  255. elseif (subdomain_policy == 'quarantine') then
  256. result.dmarc_policy = 'quarantine'
  257. elseif (subdomain_policy == 'none') then
  258. result.dmarc_policy = 'none'
  259. elseif (subdomain_policy ~= 'none') then
  260. failed_policy = 'sp tag has invalid value: ' .. subdomain_policy
  261. return false,failed_policy
  262. end
  263. end
  264. result.pct = elts['pct']
  265. if result.pct then
  266. result.pct = tonumber(result.pct)
  267. end
  268. if elts.rua then
  269. result.rua = elts['rua']
  270. end
  271. else
  272. return false,false -- Ignore garbadge
  273. end
  274. return true, result
  275. end
  276. local function dmarc_validate_policy(task, policy, hdrfromdom, dmarc_esld)
  277. local reason = {}
  278. -- Check dkim and spf symbols
  279. local spf_ok = false
  280. local dkim_ok = false
  281. local spf_tmpfail = false
  282. local dkim_tmpfail = false
  283. local spf_domain = ((task:get_from(1) or E)[1] or E).domain
  284. if not spf_domain or spf_domain == '' then
  285. spf_domain = task:get_helo() or ''
  286. end
  287. if task:has_symbol(symbols['spf_allow_symbol']) then
  288. if policy.strict_spf then
  289. if rspamd_util.strequal_caseless(spf_domain, hdrfromdom) then
  290. spf_ok = true
  291. else
  292. table.insert(reason, "SPF not aligned (strict)")
  293. end
  294. else
  295. local spf_tld = rspamd_util.get_tld(spf_domain)
  296. if rspamd_util.strequal_caseless(spf_tld, dmarc_esld) then
  297. spf_ok = true
  298. else
  299. table.insert(reason, "SPF not aligned (relaxed)")
  300. end
  301. end
  302. else
  303. if task:has_symbol(symbols['spf_tempfail_symbol']) then
  304. if policy.strict_spf then
  305. if rspamd_util.strequal_caseless(spf_domain, hdrfromdom) then
  306. spf_tmpfail = true
  307. end
  308. else
  309. local spf_tld = rspamd_util.get_tld(spf_domain)
  310. if rspamd_util.strequal_caseless(spf_tld, dmarc_esld) then
  311. spf_tmpfail = true
  312. end
  313. end
  314. end
  315. table.insert(reason, "No valid SPF")
  316. end
  317. local opts = ((task:get_symbol('DKIM_TRACE') or E)[1] or E).options
  318. local dkim_results = {
  319. pass = {},
  320. temperror = {},
  321. permerror = {},
  322. fail = {},
  323. }
  324. if opts then
  325. dkim_results.pass = {}
  326. local dkim_violated
  327. for _,opt in ipairs(opts) do
  328. local check_res = string.sub(opt, -1)
  329. local domain = string.sub(opt, 1, -3)
  330. if check_res == '+' then
  331. table.insert(dkim_results.pass, domain)
  332. if policy.strict_dkim then
  333. if rspamd_util.strequal_caseless(hdrfromdom, domain) then
  334. dkim_ok = true
  335. else
  336. dkim_violated = "DKIM not aligned (strict)"
  337. end
  338. else
  339. local dkim_tld = rspamd_util.get_tld(domain)
  340. if rspamd_util.strequal_caseless(dkim_tld, dmarc_esld) then
  341. dkim_ok = true
  342. else
  343. dkim_violated = "DKIM not aligned (relaxed)"
  344. end
  345. end
  346. elseif check_res == '?' then
  347. -- Check for dkim tempfail
  348. if not dkim_ok then
  349. if policy.strict_dkim then
  350. if rspamd_util.strequal_caseless(hdrfromdom, domain) then
  351. dkim_tmpfail = true
  352. end
  353. else
  354. local dkim_tld = rspamd_util.get_tld(domain)
  355. if rspamd_util.strequal_caseless(dkim_tld, dmarc_esld) then
  356. dkim_tmpfail = true
  357. end
  358. end
  359. end
  360. table.insert(dkim_results.temperror, domain)
  361. elseif check_res == '-' then
  362. table.insert(dkim_results.fail, domain)
  363. else
  364. table.insert(dkim_results.permerror, domain)
  365. end
  366. end
  367. if not dkim_ok and dkim_violated then
  368. table.insert(reason, dkim_violated)
  369. end
  370. else
  371. table.insert(reason, "No valid DKIM")
  372. end
  373. lua_util.debugm(N, task,
  374. "validated dmarc policy for %s: %s; dkim_ok=%s, dkim_tempfail=%s, spf_ok=%s, spf_tempfail=%s",
  375. policy.domain, policy.dmarc_policy,
  376. dkim_ok, dkim_tmpfail,
  377. spf_ok, spf_tmpfail)
  378. local disposition = 'none'
  379. local sampled_out = false
  380. local function handle_dmarc_failure(what, reason_str)
  381. if not policy.pct or policy.pct == 100 then
  382. task:insert_result(dmarc_symbols[what], 1.0,
  383. policy.domain .. ' : ' .. reason_str, policy.dmarc_policy)
  384. disposition = what
  385. else
  386. local coin = math.random(100)
  387. if (coin > policy.pct) then
  388. if (not no_sampling_domains or
  389. not no_sampling_domains:get_key(policy.domain)) then
  390. if what == 'reject' then
  391. disposition = 'quarantine'
  392. else
  393. disposition = 'softfail'
  394. end
  395. task:insert_result(dmarc_symbols[disposition], 1.0,
  396. policy.domain .. ' : ' .. reason_str, policy.dmarc_policy, "sampled_out")
  397. sampled_out = true
  398. lua_util.debugm(N, task,
  399. 'changed dmarc policy from %s to %s, sampled out: %s < %s',
  400. what, disposition, coin, policy.pct)
  401. else
  402. task:insert_result(dmarc_symbols[what], 1.0,
  403. policy.domain .. ' : ' .. reason_str, policy.dmarc_policy, "local_policy")
  404. disposition = what
  405. end
  406. else
  407. task:insert_result(dmarc_symbols[what], 1.0,
  408. policy.domain .. ' : ' .. reason_str, policy.dmarc_policy)
  409. disposition = what
  410. end
  411. end
  412. maybe_force_action(task, disposition)
  413. end
  414. if spf_ok or dkim_ok then
  415. --[[
  416. https://tools.ietf.org/html/rfc7489#section-6.6.2
  417. DMARC evaluation can only yield a "pass" result after one of the
  418. underlying authentication mechanisms passes for an aligned
  419. identifier.
  420. ]]--
  421. task:insert_result(dmarc_symbols['allow'], 1.0, policy.domain,
  422. policy.dmarc_policy)
  423. else
  424. --[[
  425. https://tools.ietf.org/html/rfc7489#section-6.6.2
  426. If neither passes and one or both of them fail due to a
  427. temporary error, the Receiver evaluating the message is unable to
  428. conclude that the DMARC mechanism had a permanent failure; they
  429. therefore cannot apply the advertised DMARC policy.
  430. ]]--
  431. if spf_tmpfail or dkim_tmpfail then
  432. task:insert_result(dmarc_symbols['dnsfail'], 1.0, policy.domain..
  433. ' : ' .. 'SPF/DKIM temp error', policy.dmarc_policy)
  434. else
  435. -- We can now check the failed policy and maybe send report data elt
  436. local reason_str = table.concat(reason, ', ')
  437. if policy.dmarc_policy == 'quarantine' then
  438. handle_dmarc_failure('quarantine', reason_str)
  439. elseif policy.dmarc_policy == 'reject' then
  440. handle_dmarc_failure('reject', reason_str)
  441. else
  442. task:insert_result(dmarc_symbols['softfail'], 1.0,
  443. policy.domain .. ' : ' .. reason_str,
  444. policy.dmarc_policy)
  445. end
  446. end
  447. end
  448. if policy.rua and redis_params and dmarc_reporting then
  449. if no_reporting_domains then
  450. if no_reporting_domains:get_key(policy.domain) or
  451. no_reporting_domains:get_key(rspamd_util.get_tld(policy.domain)) then
  452. rspamd_logger.infox(task, 'DMARC reporting suppressed for %1', policy.domain)
  453. return
  454. end
  455. end
  456. local function dmarc_report_cb(err)
  457. if not err then
  458. rspamd_logger.infox(task, '<%1> dmarc report saved for %2',
  459. task:get_message_id(), hdrfromdom)
  460. else
  461. rspamd_logger.errx(task, '<%1> dmarc report is not saved for %2: %3',
  462. task:get_message_id(), hdrfromdom, err)
  463. end
  464. end
  465. local spf_result
  466. if spf_ok then
  467. spf_result = 'pass'
  468. elseif spf_tmpfail then
  469. spf_result = 'temperror'
  470. else
  471. if task:has_symbol(symbols.spf_deny_symbol) then
  472. spf_result = 'fail'
  473. elseif task:has_symbol(symbols.spf_softfail_symbol) then
  474. spf_result = 'softfail'
  475. elseif task:has_symbol(symbols.spf_neutral_symbol) then
  476. spf_result = 'neutral'
  477. elseif task:has_symbol(symbols.spf_permfail_symbol) then
  478. spf_result = 'permerror'
  479. else
  480. spf_result = 'none'
  481. end
  482. end
  483. -- Prepare and send redis report element
  484. local period = os.date('!%Y%m%d',
  485. task:get_date({format = 'connect', gmt = true}))
  486. local dmarc_domain_key = table.concat(
  487. {redis_keys.report_prefix, hdrfromdom, period}, redis_keys.join_char)
  488. local report_data = dmarc_report(task,
  489. spf_ok and 'pass' or 'fail',
  490. dkim_ok and 'pass' or 'fail',
  491. disposition,
  492. sampled_out,
  493. hdrfromdom,
  494. spf_domain,
  495. dkim_results,
  496. spf_result)
  497. local idx_key = table.concat({redis_keys.index_prefix, period},
  498. redis_keys.join_char)
  499. if report_data then
  500. rspamd_redis.exec_redis_script(take_report_id,
  501. {task = task, is_write = true},
  502. dmarc_report_cb,
  503. {idx_key, dmarc_domain_key},
  504. {hdrfromdom, report_data})
  505. end
  506. end
  507. end
  508. local function dmarc_callback(task)
  509. local from = task:get_from(2)
  510. local hfromdom = ((from or E)[1] or E).domain
  511. local dmarc_domain
  512. local ip_addr = task:get_ip()
  513. local dmarc_checks = task:get_mempool():get_variable('dmarc_checks', 'double') or 0
  514. local seen_invalid = false
  515. if dmarc_checks ~= 2 then
  516. rspamd_logger.infox(task, "skip DMARC checks as either SPF or DKIM were not checked")
  517. return
  518. end
  519. if lua_util.is_skip_local_or_authed(task, auth_and_local_conf, ip_addr) then
  520. rspamd_logger.infox(task, "skip DMARC checks for local networks and authorized users")
  521. return
  522. end
  523. -- Do some initial sanity checks, detect tld domain if different
  524. if hfromdom and hfromdom ~= '' and not (from or E)[2] then
  525. dmarc_domain = rspamd_util.get_tld(hfromdom)
  526. elseif (from or E)[2] then
  527. task:insert_result(dmarc_symbols['na'], 1.0, 'Duplicate From header')
  528. return maybe_force_action(task, 'na')
  529. elseif (from or E)[1] then
  530. task:insert_result(dmarc_symbols['na'], 1.0, 'No domain in From header')
  531. return maybe_force_action(task,'na')
  532. else
  533. task:insert_result(dmarc_symbols['na'], 1.0, 'No From header')
  534. return maybe_force_action(task,'na')
  535. end
  536. local dns_checks_inflight = 0
  537. local dmarc_domain_policy = {}
  538. local dmarc_tld_policy = {}
  539. local function process_dmarc_policy(policy, final)
  540. lua_util.debugm(N, task, "validate DMARC policy (final=%s): %s",
  541. true, policy)
  542. if policy.err and policy.symbol then
  543. -- In case of fatal errors or final check for tld, we give up and
  544. -- insert result
  545. if final or policy.fatal then
  546. task:insert_result(policy.symbol, 1.0, policy.err)
  547. maybe_force_action(task, policy.disposition)
  548. return true
  549. end
  550. elseif policy.dmarc_policy then
  551. dmarc_validate_policy(task, policy, hfromdom, dmarc_domain)
  552. return true -- We have a more specific version, use it
  553. end
  554. return false -- Missing record
  555. end
  556. local function gen_dmarc_cb(lookup_domain, is_tld)
  557. local policy_target = dmarc_domain_policy
  558. if is_tld then
  559. policy_target = dmarc_tld_policy
  560. end
  561. return function (_, _, results, err)
  562. dns_checks_inflight = dns_checks_inflight - 1
  563. if not seen_invalid then
  564. policy_target.domain = lookup_domain
  565. if err then
  566. if (err ~= 'requested record is not found' and
  567. err ~= 'no records with this name') then
  568. policy_target.err = lookup_domain .. ' : ' .. err
  569. policy_target.symbol = dmarc_symbols['dnsfail']
  570. else
  571. policy_target.err = lookup_domain
  572. policy_target.symbol = dmarc_symbols['na']
  573. end
  574. else
  575. local has_valid_policy = false
  576. for _,rec in ipairs(results) do
  577. local ret,results_or_err = dmarc_check_record(task, rec, is_tld)
  578. if not ret then
  579. if results_or_err then
  580. -- We have a fatal parsing error, give up
  581. policy_target.err = lookup_domain .. ' : ' .. results_or_err
  582. policy_target.symbol = dmarc_symbols['badpolicy']
  583. policy_target.fatal = true
  584. seen_invalid = true
  585. end
  586. else
  587. if has_valid_policy then
  588. policy_target.err = lookup_domain .. ' : ' ..
  589. 'Multiple policies defined in DNS'
  590. policy_target.symbol = dmarc_symbols['badpolicy']
  591. policy_target.fatal = true
  592. seen_invalid = true
  593. end
  594. has_valid_policy = true
  595. for k,v in pairs(results_or_err) do
  596. policy_target[k] = v
  597. end
  598. end
  599. end
  600. if not has_valid_policy and not seen_invalid then
  601. policy_target.err = lookup_domain .. ':' .. ' no valid DMARC record'
  602. policy_target.symbol = dmarc_symbols['na']
  603. end
  604. end
  605. end
  606. if dns_checks_inflight == 0 then
  607. lua_util.debugm(N, task, "finished DNS queries, validate policies")
  608. -- We have checked both tld and real domain (if different)
  609. if not process_dmarc_policy(dmarc_domain_policy, false) then
  610. -- Try tld policy as well
  611. if not process_dmarc_policy(dmarc_tld_policy, true) then
  612. process_dmarc_policy(dmarc_domain_policy, true)
  613. end
  614. end
  615. end
  616. end
  617. end
  618. local resolve_name = '_dmarc.' .. hfromdom
  619. task:get_resolver():resolve_txt({
  620. task=task,
  621. name = resolve_name,
  622. callback = gen_dmarc_cb(hfromdom, false),
  623. forced = true
  624. })
  625. dns_checks_inflight = dns_checks_inflight + 1
  626. if dmarc_domain ~= hfromdom then
  627. resolve_name = '_dmarc.' .. dmarc_domain
  628. task:get_resolver():resolve_txt({
  629. task=task,
  630. name = resolve_name,
  631. callback = gen_dmarc_cb(dmarc_domain, true),
  632. forced = true
  633. })
  634. dns_checks_inflight = dns_checks_inflight + 1
  635. end
  636. end
  637. local opts = rspamd_config:get_all_opt('dmarc')
  638. if not opts or type(opts) ~= 'table' then
  639. return
  640. end
  641. auth_and_local_conf = lua_util.config_check_local_or_authed(rspamd_config, N,
  642. false, false)
  643. no_sampling_domains = rspamd_map_add(N, 'no_sampling_domains', 'map', 'Domains not to apply DMARC sampling to')
  644. no_reporting_domains = rspamd_map_add(N, 'no_reporting_domains', 'map', 'Domains not to apply DMARC reporting to')
  645. if opts['symbols'] then
  646. for k,_ in pairs(dmarc_symbols) do
  647. if opts['symbols'][k] then
  648. dmarc_symbols[k] = opts['symbols'][k]
  649. end
  650. end
  651. end
  652. -- XXX: rework this shitty code some day please
  653. if opts['reporting'] == true then
  654. redis_params = rspamd_parse_redis_server('dmarc')
  655. if not redis_params then
  656. rspamd_logger.errx(rspamd_config, 'cannot parse servers parameter')
  657. elseif not opts['send_reports'] then
  658. dmarc_reporting = true
  659. take_report_id = rspamd_redis.add_redis_script(take_report_script, redis_params)
  660. else
  661. dmarc_reporting = true
  662. if type(opts['report_settings']) == 'table' then
  663. for k, v in pairs(opts['report_settings']) do
  664. report_settings[k] = v
  665. end
  666. end
  667. for _, e in ipairs({'email', 'domain', 'org_name'}) do
  668. if not report_settings[e] then
  669. rspamd_logger.errx(rspamd_config, 'Missing required setting: report_settings.%s', e)
  670. return
  671. end
  672. end
  673. take_report_id = rspamd_redis.add_redis_script(take_report_script, redis_params)
  674. rspamd_config:add_on_load(function(cfg, ev_base, worker)
  675. if not worker:is_primary_controller() then return end
  676. pool = mempool.create()
  677. rspamd_config:register_finish_script(function ()
  678. local stamp = pool:get_variable(VAR_NAME, 'double')
  679. if not stamp then
  680. rspamd_logger.warnx(rspamd_config, 'No last DMARC report information to persist to disk')
  681. return
  682. end
  683. local f, err = io.open(statefile, 'w')
  684. if err then
  685. rspamd_logger.errx(rspamd_config, 'Unable to write statefile to disk: %s', err)
  686. return
  687. end
  688. assert(f:write(pool:get_variable(VAR_NAME, 'double')))
  689. assert(f:close())
  690. pool:destroy()
  691. end)
  692. local get_reporting_domain, reporting_domain, report_start,
  693. report_end, report_id, want_period, report_key
  694. local reporting_addrs = {}
  695. local bcc_addrs = {}
  696. local domain_policy = {}
  697. local to_verify = {}
  698. local cursor = 0
  699. local function entry_to_xml(data)
  700. local buf = {
  701. table.concat({
  702. '<record><row><source_ip>', data.ip, '</source_ip><count>',
  703. data.count, '</count><policy_evaluated><disposition>',
  704. data.disposition, '</disposition><dkim>', data.dkim_disposition,
  705. '</dkim><spf>', data.spf_disposition, '</spf>'
  706. }),
  707. }
  708. if data.override ~= '' then
  709. table.insert(buf, string.format('<reason><type>%s</type></reason>', data.override))
  710. end
  711. table.insert(buf, table.concat({
  712. '</policy_evaluated></row><identifiers><header_from>', data.header_from,
  713. '</header_from></identifiers>',
  714. }))
  715. table.insert(buf, '<auth_results>')
  716. if data.dkim_results[1] then
  717. for _, d in ipairs(data.dkim_results) do
  718. table.insert(buf, table.concat({
  719. '<dkim><domain>', d.domain, '</domain><result>',
  720. d.result, '</result></dkim>',
  721. }))
  722. end
  723. end
  724. table.insert(buf, table.concat({
  725. '<spf><domain>', data.spf_domain, '</domain><result>',
  726. data.spf_result, '</result></spf></auth_results></record>',
  727. }))
  728. return table.concat(buf)
  729. end
  730. local function dmarc_report_xml()
  731. local entries = {}
  732. report_id = string.format('%s.%d.%d',
  733. reporting_domain, report_start, report_end)
  734. lua_util.debugm(N, rspamd_config, 'new report: %s', report_id)
  735. local actions = {
  736. push = function(t)
  737. local data = t[1]
  738. local split = rspamd_str_split(data, ',')
  739. local row = {
  740. ip = split[1],
  741. spf_disposition = split[2],
  742. dkim_disposition = split[3],
  743. disposition = split[4],
  744. override = split[5],
  745. header_from = split[6],
  746. dkim_results = {},
  747. spf_domain = split[11],
  748. spf_result = split[12],
  749. count = t[2],
  750. }
  751. if split[7] and split[7] ~= '' then
  752. local tmp = rspamd_str_split(split[7], '|')
  753. for _, d in ipairs(tmp) do
  754. table.insert(row.dkim_results, {domain = d, result = 'pass'})
  755. end
  756. end
  757. if split[8] and split[8] ~= '' then
  758. local tmp = rspamd_str_split(split[8], '|')
  759. for _, d in ipairs(tmp) do
  760. table.insert(row.dkim_results, {domain = d, result = 'fail'})
  761. end
  762. end
  763. if split[9] and split[9] ~= '' then
  764. local tmp = rspamd_str_split(split[9], '|')
  765. for _, d in ipairs(tmp) do
  766. table.insert(row.dkim_results, {domain = d, result = 'temperror'})
  767. end
  768. end
  769. if split[10] and split[10] ~= '' then
  770. local tmp = lua_util.str_split(split[10], '|')
  771. for _, d in ipairs(tmp) do
  772. table.insert(row.dkim_results,
  773. {domain = d, result = 'permerror'})
  774. end
  775. end
  776. table.insert(entries, row)
  777. end,
  778. -- TODO: please rework this shit
  779. header = function()
  780. return table.concat({
  781. '<?xml version="1.0" encoding="utf-8"?><feedback><report_metadata><org_name>',
  782. escape_xml(report_settings.org_name), '</org_name><email>',
  783. escape_xml(report_settings.email), '</email><report_id>',
  784. report_id, '</report_id><date_range><begin>', report_start,
  785. '</begin><end>', report_end, '</end></date_range></report_metadata><policy_published><domain>',
  786. reporting_domain, '</domain><adkim>', escape_xml(domain_policy.adkim), '</adkim><aspf>',
  787. escape_xml(domain_policy.aspf), '</aspf><p>', escape_xml(domain_policy.p),
  788. '</p><sp>', escape_xml(domain_policy.sp), '</sp><pct>',
  789. escape_xml(domain_policy.pct),
  790. '</pct></policy_published>'
  791. })
  792. end,
  793. footer = function()
  794. return [[</feedback>]]
  795. end,
  796. entries = function()
  797. local buf = {}
  798. for _, e in pairs(entries) do
  799. table.insert(buf, entry_to_xml(e))
  800. end
  801. return table.concat(buf, '')
  802. end,
  803. }
  804. return function(action, p)
  805. local f = actions[action]
  806. if not f then error('invalid action: ' .. action) end
  807. return f(p)
  808. end
  809. end
  810. local function send_report_via_email(xmlf, retry)
  811. if not retry then retry = 0 end
  812. local function sendmail_cb(ret, err)
  813. if not ret then
  814. rspamd_logger.errx(rspamd_config, "Couldn't send mail for %s: %s", err)
  815. if retry >= report_settings.retries then
  816. rspamd_logger.errx(rspamd_config, "Couldn't send mail for %s: retries exceeded", reporting_domain)
  817. return get_reporting_domain()
  818. else
  819. send_report_via_email(xmlf, retry + 1)
  820. end
  821. else
  822. get_reporting_domain()
  823. end
  824. end
  825. -- Format message
  826. local list_rcpt = lua_util.keys(reporting_addrs)
  827. local encoded = rspamd_util.encode_base64(rspamd_util.gzip_compress(
  828. table.concat(
  829. {xmlf('header'),
  830. xmlf('entries'),
  831. xmlf('footer')})), 73)
  832. local addr_string = table.concat(list_rcpt, ', ')
  833. bcc_addrs = lua_util.keys(bcc_addrs)
  834. local bcc_string
  835. if #bcc_addrs > 0 then
  836. bcc_string = table.concat(bcc_addrs, ', ')
  837. end
  838. local rhead = lua_util.jinja_template(report_template,
  839. {
  840. from_name = report_settings.from_name,
  841. from_addr = report_settings.email,
  842. rcpt = addr_string,
  843. bcc = bcc_string,
  844. reporting_domain = reporting_domain,
  845. submitter = report_settings.domain,
  846. report_id = report_id,
  847. report_date = rspamd_util.time_to_string(rspamd_util.get_time()),
  848. message_id = rspamd_util.random_hex(16) .. '@' .. report_settings.msgid_from,
  849. report_start = report_start,
  850. report_end = report_end
  851. }, true)
  852. local message = {
  853. (rhead:gsub("\n", "\r\n")),
  854. encoded,
  855. (report_footer:gsub("\n", "\r\n"))
  856. }
  857. local lua_smtp = require "lua_smtp"
  858. lua_smtp.sendmail({
  859. ev_base = ev_base,
  860. config = rspamd_config,
  861. host = report_settings.smtp,
  862. port = report_settings.smtp_port,
  863. resolver = rspamd_config:get_resolver(),
  864. from = report_settings.email,
  865. recipients = list_rcpt,
  866. helo = report_settings.helo,
  867. }, message, sendmail_cb)
  868. end
  869. local function make_report()
  870. if type(report_settings.override_address) == 'string' then
  871. reporting_addrs = { [report_settings.override_address] = true}
  872. end
  873. if type(report_settings.additional_address) == 'string' then
  874. if report_settings.additional_address_bcc then
  875. bcc_addrs[report_settings.additional_address] = true
  876. else
  877. reporting_addrs[report_settings.additional_address] = true
  878. end
  879. end
  880. rspamd_logger.infox(ev_base, 'sending report for %s <%s> (<%s> bcc)',
  881. reporting_domain, reporting_addrs, bcc_addrs)
  882. local dmarc_xml = dmarc_report_xml()
  883. local dmarc_push_cb
  884. dmarc_push_cb = function(err, data)
  885. if err then
  886. rspamd_logger.errx(ev_base, 'Redis request failed: %s', err)
  887. -- XXX: data is orphaned; replace key or delete data
  888. get_reporting_domain()
  889. elseif type(data) == 'table' then
  890. cursor = tonumber(data[1])
  891. for i = 1, #data[2], 2 do
  892. dmarc_xml('push', {data[2][i], data[2][i+1]})
  893. end
  894. if cursor ~= 0 then
  895. local ret = rspamd_redis.redis_make_request_taskless(ev_base,
  896. rspamd_config,
  897. redis_params,
  898. nil,
  899. false, -- is write
  900. dmarc_push_cb, --callback
  901. 'HSCAN', -- command
  902. {report_key, cursor, 'COUNT', report_settings.hscan_count}
  903. )
  904. if not ret then
  905. rspamd_logger.errx(ev_base, 'Failed to schedule redis request')
  906. get_reporting_domain()
  907. end
  908. else
  909. send_report_via_email(dmarc_xml)
  910. end
  911. end
  912. end
  913. local ret = rspamd_redis.redis_make_request_taskless(ev_base,
  914. rspamd_config,
  915. redis_params,
  916. nil,
  917. false, -- is write
  918. dmarc_push_cb, --callback
  919. 'HSCAN', -- command
  920. {report_key, cursor, 'COUNT', report_settings.hscan_count}
  921. )
  922. if not ret then
  923. rspamd_logger.errx(rspamd_config, 'Failed to schedule redis request')
  924. -- XXX: data is orphaned; replace key or delete data
  925. get_reporting_domain()
  926. end
  927. end
  928. local function delete_reports()
  929. local function delete_reports_cb(err)
  930. if err then
  931. rspamd_logger.errx(rspamd_config, 'Error deleting reports: %s', err)
  932. end
  933. rspamd_logger.infox(rspamd_config, 'Deleted reports for %s', reporting_domain)
  934. get_reporting_domain()
  935. end
  936. local ret = rspamd_redis.redis_make_request_taskless(ev_base,
  937. rspamd_config,
  938. redis_params,
  939. nil,
  940. true, -- is write
  941. delete_reports_cb, --callback
  942. 'DEL', -- command
  943. {report_key}
  944. )
  945. if not ret then
  946. rspamd_logger.errx(rspamd_config, 'Failed to schedule redis request')
  947. get_reporting_domain()
  948. end
  949. end
  950. local function verify_reporting_address()
  951. local function verifier(test_addr, vdom)
  952. local retry = 0
  953. local function verify_cb(resolver, to_resolve, results, err, _, authenticated)
  954. if err then
  955. if err == 'no records with this name' or err == 'requested record is not found' then
  956. rspamd_logger.infox(rspamd_config, 'Reports to %s for %s not authorised', test_addr, reporting_domain)
  957. to_verify[test_addr] = nil
  958. else
  959. rspamd_logger.errx(rspamd_config, 'Lookup error [%s]: %s', to_resolve, err)
  960. if retry < report_settings.retries then
  961. retry = retry + 1
  962. rspamd_config:get_resolver():resolve('txt', {
  963. ev_base = ev_base,
  964. name = string.format('%s._report._dmarc.%s',
  965. reporting_domain, vdom),
  966. callback = verify_cb,
  967. })
  968. else
  969. delete_reports()
  970. end
  971. end
  972. else
  973. local is_authed = false
  974. -- XXX: reporting address could be overridden
  975. for _, r in ipairs(results) do
  976. if string.match(r, 'v=DMARC1') then
  977. is_authed = true
  978. break
  979. end
  980. end
  981. if not is_authed then
  982. to_verify[test_addr] = nil
  983. rspamd_logger.infox(rspamd_config, 'Reports to %s for %s not authorised', test_addr, reporting_domain)
  984. else
  985. to_verify[test_addr] = nil
  986. reporting_addrs[test_addr] = true
  987. end
  988. end
  989. local t, nvdom = next(to_verify)
  990. if not t then
  991. if next(reporting_addrs) then
  992. make_report()
  993. else
  994. rspamd_logger.infox(rspamd_config, 'No valid reporting addresses for %s', reporting_domain)
  995. delete_reports()
  996. end
  997. else
  998. verifier(t, nvdom)
  999. end
  1000. end
  1001. rspamd_config:get_resolver():resolve('txt', {
  1002. ev_base = ev_base,
  1003. name = string.format('%s._report._dmarc.%s',
  1004. reporting_domain, vdom),
  1005. callback = verify_cb,
  1006. })
  1007. end
  1008. local t, vdom = next(to_verify)
  1009. verifier(t, vdom)
  1010. end
  1011. local function get_reporting_address()
  1012. local retry = 0
  1013. local esld = rspamd_util.get_tld(reporting_domain)
  1014. local function check_addr_cb(resolver, to_resolve, results, err, _, authenticated)
  1015. if err then
  1016. if err == 'no records with this name' or err == 'requested record is not found' then
  1017. if reporting_domain ~= esld then
  1018. rspamd_config:get_resolver():resolve('txt', {
  1019. ev_base = ev_base,
  1020. name = string.format('_dmarc.%s', esld),
  1021. callback = check_addr_cb,
  1022. })
  1023. else
  1024. rspamd_logger.errx(rspamd_config, 'No DMARC record found for %s', reporting_domain)
  1025. delete_reports()
  1026. end
  1027. else
  1028. rspamd_logger.errx(rspamd_config, 'Lookup error [%s]: %s', to_resolve, err)
  1029. if retry < report_settings.retries then
  1030. retry = retry + 1
  1031. rspamd_config:get_resolver():resolve('txt', {
  1032. ev_base = ev_base,
  1033. name = to_resolve,
  1034. callback = check_addr_cb,
  1035. })
  1036. else
  1037. rspamd_logger.errx(rspamd_config, "Couldn't get reporting address for %s: retries exceeded",
  1038. reporting_domain)
  1039. delete_reports()
  1040. end
  1041. end
  1042. else
  1043. local policy
  1044. local found_policy, failed_policy = false, false
  1045. for _, r in ipairs(results) do
  1046. local elts = dmarc_grammar:match(r)
  1047. if elts and found_policy then
  1048. failed_policy = true
  1049. elseif elts then
  1050. found_policy = true
  1051. policy = dmarc_key_value_case(elts)
  1052. end
  1053. end
  1054. if not found_policy then
  1055. rspamd_logger.errx(rspamd_config, 'No policy: %s', to_resolve)
  1056. if reporting_domain ~= esld then
  1057. rspamd_config:get_resolver():resolve('txt', {
  1058. ev_base = ev_base,
  1059. name = string.format('_dmarc.%s', esld),
  1060. callback = check_addr_cb,
  1061. })
  1062. else
  1063. delete_reports()
  1064. end
  1065. elseif failed_policy then
  1066. rspamd_logger.errx(rspamd_config, 'Duplicate policies: %s', to_resolve)
  1067. delete_reports()
  1068. elseif not policy['rua'] then
  1069. rspamd_logger.errx(rspamd_config, 'No reporting address: %s', to_resolve)
  1070. delete_reports()
  1071. else
  1072. local upool = mempool.create()
  1073. local split = rspamd_str_split(policy['rua'], ',')
  1074. for _, m in ipairs(split) do
  1075. local url = rspamd_url.create(upool, m)
  1076. if not url then
  1077. rspamd_logger.errx(rspamd_config, 'Couldnt extract reporting address: %s', policy['rua'])
  1078. else
  1079. local urlt = url:to_table()
  1080. if urlt['protocol'] ~= 'mailto' then
  1081. rspamd_logger.errx(rspamd_config, 'Invalid URL: %s', url)
  1082. else
  1083. if urlt['tld'] == rspamd_util.get_tld(reporting_domain) then
  1084. reporting_addrs[string.format('%s@%s', urlt['user'], urlt['host'])] = true
  1085. else
  1086. to_verify[string.format('%s@%s', urlt['user'], urlt['host'])] = urlt['host']
  1087. end
  1088. end
  1089. end
  1090. end
  1091. upool:destroy()
  1092. domain_policy['pct'] = policy['pct'] or 100
  1093. domain_policy['adkim'] = policy['adkim'] or 'r'
  1094. domain_policy['aspf'] = policy['aspf'] or 'r'
  1095. domain_policy['p'] = policy['p'] or 'none'
  1096. domain_policy['sp'] = policy['sp'] or 'none'
  1097. if next(to_verify) then
  1098. verify_reporting_address()
  1099. elseif next(reporting_addrs) then
  1100. make_report()
  1101. else
  1102. rspamd_logger.errx(rspamd_config, 'No reporting address for %s', reporting_domain)
  1103. delete_reports()
  1104. end
  1105. end
  1106. end
  1107. end
  1108. rspamd_config:get_resolver():resolve('txt', {
  1109. ev_base = ev_base,
  1110. name = string.format('_dmarc.%s', reporting_domain),
  1111. callback = check_addr_cb,
  1112. })
  1113. end
  1114. get_reporting_domain = function()
  1115. reporting_domain = nil
  1116. reporting_addrs = {}
  1117. domain_policy = {}
  1118. cursor = 0
  1119. local function get_reporting_domain_cb(err, data)
  1120. if err then
  1121. rspamd_logger.errx(cfg, 'Unable to get DMARC domain: %s', err)
  1122. else
  1123. if type(data) == 'userdata' then
  1124. reporting_domain = nil
  1125. else
  1126. report_key = data
  1127. local tmp = rspamd_str_split(data, redis_keys.join_char)
  1128. reporting_domain = tmp[2]
  1129. end
  1130. if not reporting_domain then
  1131. rspamd_logger.infox(cfg, 'No more domains to generate reports for')
  1132. else
  1133. get_reporting_address()
  1134. end
  1135. end
  1136. end
  1137. local idx_key = table.concat({redis_keys.index_prefix, want_period}, redis_keys.join_char)
  1138. local ret = rspamd_redis.redis_make_request_taskless(ev_base,
  1139. rspamd_config,
  1140. redis_params,
  1141. nil,
  1142. true, -- is write
  1143. get_reporting_domain_cb, --callback
  1144. 'SPOP', -- command
  1145. {idx_key}
  1146. )
  1147. if not ret then
  1148. rspamd_logger.errx(cfg, 'Unable to get DMARC domain')
  1149. end
  1150. end
  1151. local function send_reports(time)
  1152. rspamd_logger.infox(ev_base, 'sending reports ostensibly %1', time)
  1153. pool:set_variable(VAR_NAME, time)
  1154. local yesterday = os.date('!*t', rspamd_util.get_time() - INTERVAL)
  1155. local today = os.date('!*t', rspamd_util.get_time())
  1156. report_start = os.time({
  1157. year = yesterday.year,
  1158. month = yesterday.month,
  1159. day = yesterday.day,
  1160. hour = 0}) + tz_offset
  1161. report_end = os.time({
  1162. year = today.year,
  1163. month = today.month,
  1164. day = today.day,
  1165. hour = 0}) + tz_offset
  1166. want_period = table.concat({
  1167. yesterday.year,
  1168. string.format('%02d', yesterday.month),
  1169. string.format('%02d', yesterday.day)
  1170. })
  1171. get_reporting_domain()
  1172. end
  1173. -- Push reports at regular intervals
  1174. local function schedule_regular_send()
  1175. rspamd_config:add_periodic(ev_base, INTERVAL, function ()
  1176. send_reports()
  1177. return true
  1178. end)
  1179. end
  1180. -- Push reports to backend and reschedule check
  1181. local function schedule_intermediate_send(when)
  1182. rspamd_config:add_periodic(ev_base, when, function ()
  1183. schedule_regular_send()
  1184. send_reports(rspamd_util.get_time())
  1185. return false
  1186. end)
  1187. end
  1188. -- Try read statefile on startup
  1189. local stamp
  1190. local f, err = io.open(statefile, 'r')
  1191. if err then
  1192. rspamd_logger.errx('Failed to open statefile: %s', err)
  1193. end
  1194. if f then
  1195. io.input(f)
  1196. stamp = tonumber(io.read())
  1197. pool:set_variable(VAR_NAME, stamp)
  1198. end
  1199. local time = rspamd_util.get_time()
  1200. if not stamp then
  1201. lua_util.debugm(N, rspamd_config, 'No state found - sending reports immediately')
  1202. schedule_regular_send()
  1203. send_reports(time)
  1204. return
  1205. end
  1206. local delta = stamp - time + INTERVAL
  1207. if delta <= 0 then
  1208. lua_util.debugm(N, rspamd_config, 'Last send is too old - sending reports immediately')
  1209. schedule_regular_send()
  1210. send_reports(time)
  1211. return
  1212. end
  1213. lua_util.debugm(N, rspamd_config, 'Scheduling next send in %s seconds', delta)
  1214. schedule_intermediate_send(delta)
  1215. end)
  1216. end
  1217. end
  1218. if type(opts['actions']) == 'table' then
  1219. dmarc_actions = opts['actions']
  1220. end
  1221. if type(opts['report_settings']) == 'table' then
  1222. for k, v in pairs(opts['report_settings']) do
  1223. report_settings[k] = v
  1224. end
  1225. end
  1226. if opts['send_reports'] then
  1227. for _, e in ipairs({'email', 'domain', 'org_name'}) do
  1228. if not report_settings[e] then
  1229. rspamd_logger.errx(rspamd_config, 'Missing required setting: report_settings.%s', e)
  1230. return
  1231. end
  1232. end
  1233. end
  1234. -- Check spf and dkim sections for changed symbols
  1235. local function check_mopt(var, m_opts, name)
  1236. if m_opts[name] then
  1237. symbols[var] = tostring(m_opts[name])
  1238. end
  1239. end
  1240. local spf_opts = rspamd_config:get_all_opt('spf')
  1241. if spf_opts then
  1242. check_mopt('spf_deny_symbol', spf_opts, 'symbol_fail')
  1243. check_mopt('spf_allow_symbol', spf_opts, 'symbol_allow')
  1244. check_mopt('spf_softfail_symbol', spf_opts, 'symbol_softfail')
  1245. check_mopt('spf_neutral_symbol', spf_opts, 'symbol_neutral')
  1246. check_mopt('spf_tempfail_symbol', spf_opts, 'symbol_dnsfail')
  1247. check_mopt('spf_na_symbol', spf_opts, 'symbol_na')
  1248. end
  1249. local dkim_opts = rspamd_config:get_all_opt('dkim')
  1250. if dkim_opts then
  1251. check_mopt('dkim_deny_symbol', dkim_opts, 'symbol_reject')
  1252. check_mopt('dkim_allow_symbol', dkim_opts, 'symbol_allow')
  1253. check_mopt('dkim_tempfail_symbol', dkim_opts, 'symbol_tempfail')
  1254. check_mopt('dkim_na_symbol', dkim_opts, 'symbol_na')
  1255. end
  1256. local id = rspamd_config:register_symbol({
  1257. name = 'DMARC_CALLBACK',
  1258. type = 'callback',
  1259. group = 'policies',
  1260. groups = {'dmarc'},
  1261. callback = dmarc_callback
  1262. })
  1263. rspamd_config:register_symbol({
  1264. name = dmarc_symbols['allow'],
  1265. parent = id,
  1266. group = 'policies',
  1267. groups = {'dmarc'},
  1268. type = 'virtual'
  1269. })
  1270. rspamd_config:register_symbol({
  1271. name = dmarc_symbols['reject'],
  1272. parent = id,
  1273. group = 'policies',
  1274. groups = {'dmarc'},
  1275. type = 'virtual'
  1276. })
  1277. rspamd_config:register_symbol({
  1278. name = dmarc_symbols['quarantine'],
  1279. parent = id,
  1280. group = 'policies',
  1281. groups = {'dmarc'},
  1282. type = 'virtual'
  1283. })
  1284. rspamd_config:register_symbol({
  1285. name = dmarc_symbols['softfail'],
  1286. parent = id,
  1287. group = 'policies',
  1288. groups = {'dmarc'},
  1289. type = 'virtual'
  1290. })
  1291. rspamd_config:register_symbol({
  1292. name = dmarc_symbols['dnsfail'],
  1293. parent = id,
  1294. group = 'policies',
  1295. groups = {'dmarc'},
  1296. type = 'virtual'
  1297. })
  1298. rspamd_config:register_symbol({
  1299. name = dmarc_symbols['badpolicy'],
  1300. parent = id,
  1301. group = 'policies',
  1302. groups = {'dmarc'},
  1303. type = 'virtual'
  1304. })
  1305. rspamd_config:register_symbol({
  1306. name = dmarc_symbols['na'],
  1307. parent = id,
  1308. group = 'policies',
  1309. groups = {'dmarc'},
  1310. type = 'virtual'
  1311. })
  1312. rspamd_config:register_dependency('DMARC_CALLBACK', symbols['spf_allow_symbol'])
  1313. rspamd_config:register_dependency('DMARC_CALLBACK', symbols['dkim_allow_symbol'])