| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387 | --[[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.0Unless required by applicable law or agreed to in writing, softwaredistributed 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 andlimitations under the License.]]--if confighelp then  returnend-- A plugin that provides common header manipulationslocal logger = require "rspamd_logger"local util = require "rspamd_util"local N = 'milter_headers'local E = {}local HOSTNAME = util.get_hostname()local settings = {  skip_local = false,  skip_authenticated = false,  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)  if settings.skip_local then    local ip = task:get_ip()    if (ip and ip:is_local()) then return end  end  if settings.skip_authenticated then    if task:get_user() ~= nil then return end  end  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    })  endendlocal opts = rspamd_config:get_all_opt(N) or rspamd_config:get_all_opt('rmilter_headers')if not opts then return endif 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')  returnendif type(opts['use']) ~= 'table' then  logger.errx(rspamd_config, 'unexpected type for "use" option: %s', type(opts['use']))  returnendif 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  endendlocal 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)  endendif opts['extended_spam_headers'] then  activate_routine('x-spamd-result')  activate_routine('x-rspamd-server')  activate_routine('x-rspamd-queue-id')endif opts['skip_local'] then  settings.skip_local = trueendif opts['skip_authenticated'] then  settings.skip_authenticated = trueendfor _, s in ipairs(opts['use']) do  if not have_routine[s] then    activate_routine(s)  endendif (#active_routines < 1) then  logger.errx(rspamd_config, 'no active routines')  returnendlogger.infox(rspamd_config, 'active routines [%s]', table.concat(active_routines, ','))rspamd_config:register_symbol({  name = 'MILTER_HEADERS',  type = 'postfilter',  callback = milter_headers,  priority = 10})
 |