diff options
Diffstat (limited to 'src/plugins/lua/milter_headers.lua')
-rw-r--r-- | src/plugins/lua/milter_headers.lua | 370 |
1 files changed, 370 insertions, 0 deletions
diff --git a/src/plugins/lua/milter_headers.lua b/src/plugins/lua/milter_headers.lua new file mode 100644 index 000000000..7447cd208 --- /dev/null +++ b/src/plugins/lua/milter_headers.lua @@ -0,0 +1,370 @@ +--[[ +Copyright (c) 2016, Andrew Lewis <nerf@judo.za.org> +Copyright (c) 2016, Vsevolod Stakhov <vsevolod@highsecure.ru> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +]]-- + +if confighelp then + return +end + +-- A plugin that provides common header manipulations + +local logger = require "rspamd_logger" +local util = require "rspamd_util" +local N = 'milter_headers' +local E = {} + +local HOSTNAME = util.get_hostname() + +local settings = { + routines = { + ['x-spamd-result'] = { + header = 'X-Spamd-Result', + remove = 1, + }, + ['x-rspamd-server'] = { + header = 'X-Rspamd-Server', + remove = 1, + }, + ['x-rspamd-queue-id'] = { + header = 'X-Rspamd-Queue-Id', + remove = 1, + }, + ['spam-header'] = { + header = 'Deliver-To', + value = 'Junk', + remove = 1, + }, + ['x-virus'] = { + header = 'X-Virus', + remove = 1, + symbols = {}, -- needs config + }, + ['x-spamd-bar'] = { + header = 'X-Spamd-Bar', + positive = '+', + negative = '-', + neutral = '/', + remove = 1, + }, + ['x-spam-level'] = { + header = 'X-Spam-Level', + char = '*', + remove = 1, + }, + ['x-spam-status'] = { + header = 'X-Spam-Status', + remove = 1, + }, + ['authentication-results'] = { + header = 'Authentication-Results', + remove = 1, + spf_symbols = { + pass = 'R_SPF_ALLOW', + fail = 'R_SPF_FAIL', + softfail = 'R_SPF_SOFTFAIL', + neutral = 'R_SPF_NEUTRAL', + temperror = 'R_SPF_DNSFAIL', + none = 'R_SPF_NA', + permerror = 'R_SPF_PERMFAIL', + }, + dkim_symbols = { + pass = 'R_DKIM_ALLOW', + fail = 'R_DKIM_REJECT', + temperror = 'R_DKIM_TEMPFAIL', + none = 'R_DKIM_NA', + permerror = 'R_DKIM_PERMFAIL', + }, + dmarc_symbols = { + pass = 'DMARC_POLICY_ALLOW', + permerror = 'DMARC_BAD_POLICY', + temperror = 'DMARC_DNSFAIL', + none = 'DMARC_NA', + reject = 'DMARC_POLICY_REJECT', + softfail = 'DMARC_POLICY_SOFTFAIL', + quarantine = 'DMARC_POLICY_QUARANTINE', + }, + }, + }, +} + +local active_routines = {} +local custom_routines = {} + +local function milter_headers(task) + + local routines, common, add, remove = {}, {}, {}, {} + + routines['x-spamd-result'] = function() + if not common.symbols then + common.symbols = task:get_symbols_all() + common['metric_score'] = task:get_metric_score('default') + common['metric_action'] = task:get_metric_action('default') + end + if settings.routines['x-spamd-result'].remove then + remove[settings.routines['x-spamd-result'].header] = settings.routines['x-spamd-result'].remove + end + local buf = {} + table.insert(buf, table.concat({ + 'default: ', (common['metric_action'] == 'reject') and 'True' or 'False', ' [', + common['metric_score'][1], ' / ', common['metric_score'][2], ']' + })) + for _, s in ipairs(common.symbols) do + if not s.options then s.options = {} end + table.insert(buf, table.concat({ + ' ', s.name, ' (', s.score, ') [', table.concat(s.options, ','), ']', + })) + end + add[settings.routines['x-spamd-result'].header] = table.concat(buf, '\n') + end + + routines['x-rspamd-queue-id'] = function() + if common.queue_id ~= false then + common.queue_id = task:get_queue_id() + if not common.queue_id then + common.queue_id = false + end + end + if settings.routines['x-rspamd-queue-id'].remove then + remove[settings.routines['x-rspamd-queue-id'].header] = settings.routines['x-rspamd-queue-id'].remove + end + if common.queue_id then + add[settings.routines['x-rspamd-queue-id'].header] = common.queue_id + end + end + + routines['x-rspamd-server'] = function() + if settings.routines['x-rspamd-server'].remove then + remove[settings.routines['x-rspamd-server'].header] = settings.routines['x-rspamd-server'].remove + end + add[settings.routines['x-rspamd-server'].header] = HOSTNAME + end + + routines['x-spamd-bar'] = function() + if not common['metric_score'] then + common['metric_score'] = task:get_metric_score('default') + end + local score = common['metric_score'][1] + local spambar + if score <= -1 then + spambar = string.rep(settings.routines['x-spamd-bar'].negative, score*-1) + elseif score >= 1 then + spambar = string.rep(settings.routines['x-spamd-bar'].positive, score) + else + spambar = settings.routines['x-spamd-bar'].neutral + end + if settings.routines['x-spamd-bar'].remove then + remove[settings.routines['x-spamd-bar'].header] = settings.routines['x-spamd-bar'].remove + end + if spambar ~= '' then + add[settings.routines['x-spamd-bar'].header] = spambar + end + end + + routines['x-spam-level'] = function() + if not common['metric_score'] then + common['metric_score'] = task:get_metric_score('default') + end + local score = common['metric_score'][1] + if score < 1 then + return nil, {}, {} + end + if settings.routines['x-spam-level'].remove then + remove[settings.routines['x-spam-level'].header] = settings.routines['x-spam-level'].remove + end + add[settings.routines['x-spam-level'].header] = string.rep(settings.routines['x-spam-level'].char, score) + end + + routines['spam-header'] = function() + if not common['metric_action'] then + common['metric_action'] = task:get_metric_action('default') + end + if settings.routines['spam-header'].remove then + remove[settings.routines['spam-header'].header] = settings.routines['spam-header'].remove + end + local action = common['metric_action'] + if action ~= 'no action' and action ~= 'greylist' then + add[settings.routines['spam-header'].header] = settings.routines['spam-header'].value + end + end + + routines['x-virus'] = function() + if not common.symbols then + common.symbols = {} + end + if settings.routines['x-virus'].remove then + remove[settings.routines['x-virus'].header] = settings.routines['x-virus'].remove + end + local virii = {} + for _, sym in ipairs(settings.routines['x-virus'].symbols) do + if not (common.symbols[sym] == false) then + local s = task:get_symbol(sym) + if not s then + common.symbols[sym] = false + else + common.symbols[sym] = s + if (((s or E)[1] or E).options or E)[1] then + table.insert(virii, s[1].options[1]) + else + table.insert(virii, 'unknown') + end + end + end + end + if #virii > 0 then + add[settings.routines['x-virus'].header] = table.concat(virii, ',') + end + end + + routines['x-spam-status'] = function() + if not common['metric_score'] then + common['metric_score'] = task:get_metric_score('default') + end + if not common['metric_action'] then + common['metric_action'] = task:get_metric_action('default') + end + local score = common['metric_score'][1] + local action = common['metric_action'] + local is_spam + local spamstatus + if action ~= 'no action' and action ~= 'greylist' then + is_spam = 'Yes' + else + is_spam = 'No' + end + spamstatus = is_spam .. ', score=' .. string.format('%.2f', score) + if settings.routines['x-spam-status'].remove then + remove[settings.routines['x-spam-status'].header] = settings.routines['x-spam-status'].remove + end + add[settings.routines['x-spam-status'].header] = spamstatus + end + + routines['authentication-results'] = function() + local ar = require "auth_results" + + if settings.routines['authentication-results'].remove then + remove[settings.routines['authentication-results'].header] = + settings.routines['authentication-results'].remove + end + + local res = ar.gen_auth_results(task, + settings.routines['authentication-results']) + + if res then + add[settings.routines['authentication-results'].header] = res + end + end + + for _, n in ipairs(active_routines) do + local ok, err + if custom_routines[n] then + local to_add, to_remove, common_in + ok, err, to_add, to_remove, common_in = pcall(custom_routines[n], task, common) + if ok then + for k, v in pairs(to_add) do + add[k] = v + end + for k, v in pairs(to_remove) do + add[k] = v + end + for k, v in pairs(common_in) do + if type(v) == 'table' then + if not common[k] then + common[k] = {} + end + for kk, vv in pairs(v) do + common[k][kk] = vv + end + else + common[k] = v + end + end + end + else + ok, err = pcall(routines[n]) + end + if not ok then + logger.errx(task, 'call to %s failed: %s', n, err) + end + end + + if not next(add) then add = nil end + if not next(remove) then remove = nil end + if add or remove then + task:set_milter_reply({ + add_headers = add, + remove_headers = remove + }) + end +end + +local opts = rspamd_config:get_all_opt(N) or rspamd_config:get_all_opt('rmilter_headers') +if not opts then return end + +if type(opts['use']) == 'string' then + opts['use'] = {opts['use']} +elseif (type(opts['use']) == 'table' and not opts['use'][1]) then + logger.debugm(N, rspamd_config, 'no functions are enabled') + return +end +if type(opts['use']) ~= 'table' then + logger.errx(rspamd_config, 'unexpected type for "use" option: %s', type(opts['use'])) + return +end +if type(opts['custom']) == 'table' then + for k, v in pairs(opts['custom']) do + local f, err = load(v) + if not f then + logger.errx(rspamd_config, 'could not load "%s": %s', k, err) + else + custom_routines[k] = f() + end + end +end +local have_routine = {} +local function activate_routine(s) + if settings.routines[s] or custom_routines[s] then + have_routine[s] = true + table.insert(active_routines, s) + if (opts.routines and opts.routines[s]) then + for k, v in pairs(opts.routines[s]) do + settings.routines[s][k] = v + end + end + else + logger.errx(rspamd_config, 'routine "%s" does not exist', s) + end +end +if opts['extended_spam_headers'] then + activate_routine('x-spamd-result') + activate_routine('x-rspamd-server') + activate_routine('x-rspamd-queue-id') +end +for _, s in ipairs(opts['use']) do + if not have_routine[s] then + activate_routine(s) + end +end +if (#active_routines < 1) then + logger.errx(rspamd_config, 'no active routines') + return +end +logger.infox(rspamd_config, 'active routines [%s]', table.concat(active_routines, ',')) +rspamd_config:register_symbol({ + name = 'MILTER_HEADERS', + type = 'postfilter', + callback = milter_headers, + priority = 10 +}) |