diff options
Diffstat (limited to '')
-rw-r--r-- | lualib/plugins/rbl.lua | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/lualib/plugins/rbl.lua b/lualib/plugins/rbl.lua new file mode 100644 index 0000000..af5d6bd --- /dev/null +++ b/lualib/plugins/rbl.lua @@ -0,0 +1,232 @@ +--[[ +Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com> + +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. +]]-- + +local ts = require("tableshape").types +local lua_maps = require "lua_maps" +local lua_util = require "lua_util" + +-- Common RBL plugin definitions + +local check_types = { + from = { + connfilter = true, + }, + received = {}, + helo = { + connfilter = true, + }, + urls = {}, + content_urls = {}, + numeric_urls = {}, + emails = {}, + replyto = {}, + dkim = {}, + rdns = { + connfilter = true, + }, + selector = { + require_argument = true, + }, +} + +local default_options = { + ['default_enabled'] = true, + ['default_ipv4'] = true, + ['default_ipv6'] = true, + ['default_unknown'] = false, + ['default_dkim_domainonly'] = true, + ['default_emails_domainonly'] = false, + ['default_exclude_users'] = false, + ['default_exclude_local'] = true, + ['default_no_ip'] = false, + ['default_dkim_match_from'] = false, + ['default_selector_flatten'] = true, +} + +local return_codes_schema = ts.map_of( + ts.string / string.upper, -- Symbol name + ( + ts.array_of(ts.string) + + (ts.string / function(s) + return { s } + end) -- List of IP patterns + ) +) +local return_bits_schema = ts.map_of( + ts.string / string.upper, -- Symbol name + ( + ts.array_of(ts.number + ts.string / tonumber) + + (ts.string / function(s) + return { tonumber(s) } + end) + + (ts.number / function(s) + return { s } + end) + ) +) + +local rule_schema_tbl = { + content_urls = ts.boolean:is_optional(), + disable_monitoring = ts.boolean:is_optional(), + disabled = ts.boolean:is_optional(), + dkim = ts.boolean:is_optional(), + dkim_domainonly = ts.boolean:is_optional(), + dkim_match_from = ts.boolean:is_optional(), + emails = ts.boolean:is_optional(), + emails_delimiter = ts.string:is_optional(), + emails_domainonly = ts.boolean:is_optional(), + enabled = ts.boolean:is_optional(), + exclude_local = ts.boolean:is_optional(), + exclude_users = ts.boolean:is_optional(), + from = ts.boolean:is_optional(), + hash = ts.one_of { "sha1", "sha256", "sha384", "sha512", "md5", "blake2" }:is_optional(), + hash_format = ts.one_of { "hex", "base32", "base64" }:is_optional(), + hash_len = (ts.integer + ts.string / tonumber):is_optional(), + helo = ts.boolean:is_optional(), + ignore_default = ts.boolean:is_optional(), -- alias + ignore_defaults = ts.boolean:is_optional(), + ignore_url_whitelist = ts.boolean:is_optional(), + ignore_whitelist = ts.boolean:is_optional(), + ignore_whitelists = ts.boolean:is_optional(), -- alias + images = ts.boolean:is_optional(), + ipv4 = ts.boolean:is_optional(), + ipv6 = ts.boolean:is_optional(), + is_whitelist = ts.boolean:is_optional(), + local_exclude_ip_map = ts.string:is_optional(), + monitored_address = ts.string:is_optional(), + no_ip = ts.boolean:is_optional(), + process_script = ts.string:is_optional(), + random_monitored = ts.boolean:is_optional(), + rbl = ts.string, + rdns = ts.boolean:is_optional(), + received = ts.boolean:is_optional(), + received_flags = ts.array_of(ts.string):is_optional(), + received_max_pos = ts.number:is_optional(), + received_min_pos = ts.number:is_optional(), + received_nflags = ts.array_of(ts.string):is_optional(), + replyto = ts.boolean:is_optional(), + requests_limit = (ts.integer + ts.string / tonumber):is_optional(), + require_symbols = ( + ts.array_of(ts.string) + (ts.string / function(s) + return { s } + end) + ):is_optional(), + resolve_ip = ts.boolean:is_optional(), + return_bits = return_bits_schema:is_optional(), + return_codes = return_codes_schema:is_optional(), + returnbits = return_bits_schema:is_optional(), + returncodes = return_codes_schema:is_optional(), + returncodes_matcher = ts.one_of { "equality", "glob", "luapattern", "radix", "regexp" }:is_optional(), + selector = ts.one_of { ts.string, ts.table }:is_optional(), + selector_flatten = ts.boolean:is_optional(), + symbol = ts.string:is_optional(), + symbols_prefixes = ts.map_of(ts.string, ts.string):is_optional(), + unknown = ts.boolean:is_optional(), + url_compose_map = lua_maps.map_schema:is_optional(), + url_full_hostname = ts.boolean:is_optional(), + url_whitelist = lua_maps.map_schema:is_optional(), + urls = ts.boolean:is_optional(), + whitelist = lua_maps.map_schema:is_optional(), + whitelist_exception = ( + ts.array_of(ts.string) + (ts.string / function(s) + return { s } + end) + ):is_optional(), + checks = ts.array_of(ts.one_of(lua_util.keys(check_types))):is_optional(), + exclude_checks = ts.array_of(ts.one_of(lua_util.keys(check_types))):is_optional(), +} + +local function convert_checks(rule, name) + local rspamd_logger = require "rspamd_logger" + if rule.checks then + local all_connfilter = true + local exclude_checks = lua_util.list_to_hash(rule.exclude_checks or {}) + for _, check in ipairs(rule.checks) do + if not exclude_checks[check] then + local check_type = check_types[check] + if check_type.require_argument then + if not rule[check] then + rspamd_logger.errx(rspamd_config, 'rbl rule %s has check %s which requires an argument', + name, check) + return nil + end + end + + rule[check] = check_type + + if not check_type.connfilter then + all_connfilter = false + end + + if not check_type then + rspamd_logger.errx(rspamd_config, 'rbl rule %s has invalid check type: %s', + name, check) + return nil + end + else + rspamd_logger.infox(rspamd_config, 'disable check %s in %s: excluded explicitly', + check, name) + end + end + rule.connfilter = all_connfilter + end + + -- Now check if we have any check enabled at all + local check_found = false + for k, _ in pairs(check_types) do + if type(rule[k]) ~= 'nil' then + check_found = true + break + end + end + + if not check_found then + -- Enable implicit `from` check to allow upgrade + rspamd_logger.warnx(rspamd_config, 'rbl rule %s has no check enabled, enable default `from` check', + name) + rule.from = true + end + + if rule.returncodes and not rule.returncodes_matcher then + for _, v in pairs(rule.returncodes) do + for _, e in ipairs(v) do + if e:find('[%%%[]') then + rspamd_logger.warn(rspamd_config, 'implicitly enabling luapattern returncodes_matcher for rule %s', name) + rule.returncodes_matcher = 'luapattern' + break + end + end + if rule.returncodes_matcher then + break + end + end + end + + return rule +end + + +-- Add default boolean flags to the schema +for def_k, _ in pairs(default_options) do + rule_schema_tbl[def_k:sub(#('default_') + 1)] = ts.boolean:is_optional() +end + +return { + check_types = check_types, + rule_schema = ts.shape(rule_schema_tbl), + default_options = default_options, + convert_checks = convert_checks, +} |