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

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