From 817713d29c214e15194b60d848cd57bd4b3ab271 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Nov 2025 12:58:16 +0000 Subject: [PATCH] Refactor DMARC reporting to use helper functions and async maps Co-authored-by: v --- src/plugins/lua/dmarc.lua | 234 ++++++++++++++++++++++++++------------ 1 file changed, 164 insertions(+), 70 deletions(-) diff --git a/src/plugins/lua/dmarc.lua b/src/plugins/lua/dmarc.lua index cc1a67661c..da05d13bad 100644 --- a/src/plugins/lua/dmarc.lua +++ b/src/plugins/lua/dmarc.lua @@ -263,94 +263,188 @@ local function dmarc_validate_policy(task, policy, hdrfromdom, dmarc_esld) end if policy.rua and redis_params and settings.reporting.enabled then - if settings.reporting.only_domains then - if not (settings.reporting.only_domains:get_key(policy.domain) or - settings.reporting.only_domains:get_key(rspamd_util.get_tld(policy.domain))) then - rspamd_logger.info(task, 'DMARC reporting suppressed for sender domain %s', policy.domain) - return + -- Helper function to perform the actual report generation + local function generate_dmarc_report() + local function dmarc_report_cb(err) + if not err then + rspamd_logger.infox(task, 'dmarc report saved for %s (rua = %s)', + hdrfromdom, policy.rua) + else + rspamd_logger.errx(task, 'dmarc report is not saved for %s: %s', + hdrfromdom, err) + end end - end - if settings.reporting.exclude_domains then - if settings.reporting.exclude_domains:get_key(policy.domain) or - settings.reporting.exclude_domains:get_key(rspamd_util.get_tld(policy.domain)) then - rspamd_logger.info(task, 'DMARC reporting suppressed for sender domain %s', policy.domain) - return + + local spf_result + if spf_ok then + spf_result = 'pass' + elseif spf_tmpfail then + spf_result = 'temperror' + else + if task:has_symbol(settings.symbols.spf_deny_symbol) then + spf_result = 'fail' + elseif task:has_symbol(settings.symbols.spf_softfail_symbol) then + spf_result = 'softfail' + elseif task:has_symbol(settings.symbols.spf_neutral_symbol) then + spf_result = 'neutral' + elseif task:has_symbol(settings.symbols.spf_permfail_symbol) then + spf_result = 'permerror' + else + spf_result = 'none' + end end - end - if settings.reporting.exclude_recipients then - local rcpt = task:get_principal_recipient() - if rcpt and settings.reporting.exclude_recipients:get_key(rcpt) then - rspamd_logger.info(task, 'DMARC reporting suppressed for recipient %s', rcpt) - return + + -- Prepare and send redis report element + local period = os.date('%Y%m%d', + task:get_date({ format = 'connect', gmt = false })) + + -- Dmarc domain key must include dmarc domain, rua and period + local dmarc_domain_key = table.concat( + { settings.reporting.redis_keys.report_prefix, policy.domain, policy.rua, period }, + settings.reporting.redis_keys.join_char) + local report_data = dmarc_common.dmarc_report(task, settings, { + spf_ok = spf_ok and 'pass' or 'fail', + dkim_ok = dkim_ok and 'pass' or 'fail', + disposition = (disposition == "softfail") and "none" or disposition, + sampled_out = sampled_out, + domain = hdrfromdom, + spf_domain = spf_domain, + dkim_results = dkim_results, + spf_result = spf_result + }) + + local idx_key = table.concat({ settings.reporting.redis_keys.index_prefix, period }, + settings.reporting.redis_keys.join_char) + + if report_data then + lua_redis.exec_redis_script(take_report_id, + { task = task, is_write = true }, + dmarc_report_cb, + { idx_key, dmarc_domain_key, + tostring(settings.reporting.max_entries), tostring(settings.reporting.keys_expire) }, + { hdrfromdom, report_data }) end end - if policy.rua:match("^mailto:") and settings.reporting.exclude_rua_addresses then - local rua = policy.rua:gsub("^mailto:", "") - if settings.reporting.exclude_rua_addresses:get_key(rua) then - rspamd_logger.info(task, 'DMARC reporting suppressed for rua recipient %s', rua) + + -- Helper function to check a map with support for both sync and external maps + local function check_map(map_obj, key, continue_cb, suppress_reason) + if not map_obj then + -- No map configured, continue + continue_cb() return end - end - local function dmarc_report_cb(err) - if not err then - rspamd_logger.infox(task, 'dmarc report saved for %s (rua = %s)', - hdrfromdom, policy.rua) + if map_obj.__external then + -- External map, use async callback + map_obj:get_key(key, function(found, _, _, _) + if found then + rspamd_logger.infox(task, 'DMARC reporting suppressed: %s', suppress_reason) + else + continue_cb() + end + end, task) else - rspamd_logger.errx(task, 'dmarc report is not saved for %s: %s', - hdrfromdom, err) + -- Regular map, synchronous check + if map_obj:get_key(key) then + rspamd_logger.infox(task, 'DMARC reporting suppressed: %s', suppress_reason) + else + continue_cb() + end end end - local spf_result - if spf_ok then - spf_result = 'pass' - elseif spf_tmpfail then - spf_result = 'temperror' - else - if task:has_symbol(settings.symbols.spf_deny_symbol) then - spf_result = 'fail' - elseif task:has_symbol(settings.symbols.spf_softfail_symbol) then - spf_result = 'softfail' - elseif task:has_symbol(settings.symbols.spf_neutral_symbol) then - spf_result = 'neutral' - elseif task:has_symbol(settings.symbols.spf_permfail_symbol) then - spf_result = 'permerror' + -- Forward declarations for chain of exclusion checks + local check_only_domains, check_exclude_domains, check_exclude_recipients, check_exclude_rua + + -- Chain exclusion checks together + check_only_domains = function() + if settings.reporting.only_domains then + if settings.reporting.only_domains.__external then + -- Check both domain and TLD for external maps + settings.reporting.only_domains:get_key(policy.domain, function(found1, _, _, _) + if found1 then + check_exclude_domains() + else + settings.reporting.only_domains:get_key(rspamd_util.get_tld(policy.domain), function(found2, _, _, _) + if found2 then + check_exclude_domains() + else + rspamd_logger.infox(task, 'DMARC reporting suppressed for sender domain %s (not in only_domains)', policy.domain) + end + end, task) + end + end, task) + else + -- Synchronous check for regular maps + if settings.reporting.only_domains:get_key(policy.domain) or + settings.reporting.only_domains:get_key(rspamd_util.get_tld(policy.domain)) then + check_exclude_domains() + else + rspamd_logger.infox(task, 'DMARC reporting suppressed for sender domain %s (not in only_domains)', policy.domain) + end + end else - spf_result = 'none' + check_exclude_domains() end end - -- Prepare and send redis report element - local period = os.date('%Y%m%d', - task:get_date({ format = 'connect', gmt = false })) - - -- Dmarc domain key must include dmarc domain, rua and period - local dmarc_domain_key = table.concat( - { settings.reporting.redis_keys.report_prefix, policy.domain, policy.rua, period }, - settings.reporting.redis_keys.join_char) - local report_data = dmarc_common.dmarc_report(task, settings, { - spf_ok = spf_ok and 'pass' or 'fail', - dkim_ok = dkim_ok and 'pass' or 'fail', - disposition = (disposition == "softfail") and "none" or disposition, - sampled_out = sampled_out, - domain = hdrfromdom, - spf_domain = spf_domain, - dkim_results = dkim_results, - spf_result = spf_result - }) + check_exclude_domains = function() + if settings.reporting.exclude_domains then + if settings.reporting.exclude_domains.__external then + -- Check both domain and TLD for external maps + settings.reporting.exclude_domains:get_key(policy.domain, function(found1, _, _, _) + if found1 then + rspamd_logger.infox(task, 'DMARC reporting suppressed for sender domain %s', policy.domain) + else + settings.reporting.exclude_domains:get_key(rspamd_util.get_tld(policy.domain), function(found2, _, _, _) + if found2 then + rspamd_logger.infox(task, 'DMARC reporting suppressed for sender domain %s', policy.domain) + else + check_exclude_recipients() + end + end, task) + end + end, task) + else + -- Synchronous check for regular maps + if settings.reporting.exclude_domains:get_key(policy.domain) or + settings.reporting.exclude_domains:get_key(rspamd_util.get_tld(policy.domain)) then + rspamd_logger.infox(task, 'DMARC reporting suppressed for sender domain %s', policy.domain) + else + check_exclude_recipients() + end + end + else + check_exclude_recipients() + end + end - local idx_key = table.concat({ settings.reporting.redis_keys.index_prefix, period }, - settings.reporting.redis_keys.join_char) + check_exclude_recipients = function() + if settings.reporting.exclude_recipients then + local rcpt = task:get_principal_recipient() + if rcpt then + check_map(settings.reporting.exclude_recipients, rcpt, check_exclude_rua, + string.format('recipient %s', rcpt)) + else + check_exclude_rua() + end + else + check_exclude_rua() + end + end - if report_data then - lua_redis.exec_redis_script(take_report_id, - { task = task, is_write = true }, - dmarc_report_cb, - { idx_key, dmarc_domain_key, - tostring(settings.reporting.max_entries), tostring(settings.reporting.keys_expire) }, - { hdrfromdom, report_data }) + check_exclude_rua = function() + if policy.rua:match("^mailto:") and settings.reporting.exclude_rua_addresses then + local rua = policy.rua:gsub("^mailto:", "") + check_map(settings.reporting.exclude_rua_addresses, rua, generate_dmarc_report, + string.format('rua recipient %s', rua)) + else + generate_dmarc_report() + end end + + -- Start the exclusion check chain + check_only_domains() end end -- 2.47.3