From: Vsevolod Stakhov Date: Sat, 17 Sep 2016 15:35:35 +0000 (+0100) Subject: [Feature] Add plugin to check MX'es for the sender's domain X-Git-Tag: 1.4.0~415 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=e9b80391e0dc97249c40397ceaa34c2e466b34ef;p=rspamd.git [Feature] Add plugin to check MX'es for the sender's domain --- diff --git a/src/plugins/lua/mx_check.lua b/src/plugins/lua/mx_check.lua new file mode 100644 index 000000000..07eae027c --- /dev/null +++ b/src/plugins/lua/mx_check.lua @@ -0,0 +1,257 @@ +--[[ +Copyright (c) 2016, Vsevolod Stakhov + +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. +]]-- + +-- MX check plugin +local rspamd_logger = require "rspamd_logger" +local rspamd_tcp = require "rspamd_tcp" +local rspamd_redis = require "rspamd_redis" +local rspamd_util = require "rspamd_util" +require "fun" () + +local settings = { + timeout = 1.0, -- connect timeout + symbol_bad_mx = 'MX_INVALID', + symbol_no_mx = 'MX_MISSING', + symbol_good_mx = 'MX_GOOD', + expire = 86400, -- 1 day by default + key_prefix = 'rmx' +} +local redis_params + +local function mx_check(task) + local from = task:get_from('smtp') + local mx_domain + + if from and from[1] and from[1]['domain'] and not from[2] then + mx_domain = rspamd_util.get_tld(from[1]['domain']) + end + + if not mx_domain then + return + end + + local valid = false + + local function check_results(mxes) + if all(function(k, elt) return elt.checked end, mxes) then + -- Save cache + local key = settings.key_prefix .. mx_domain + local function redis_cache_cb(task, err, data) + if err ~= nil then + rspamd_logger.errx(task, 'redis_cache_cb received error: %1', err) + return + end + end + if not valid then + task:insert_result(settings.symbol_bad_mx, 1.0) + local ret,_,_ = rspamd_redis_make_request(task, + redis_params, -- connect params + key, -- hash key + false, -- is write + redis_cache_cb, --callback + 'SETEX', -- command + {key, tostring(settings.expire / 10.0), '0'} -- arguments + ) + else + local valid_mx = {} + each(function(k, mx) + table.insert(valid_mx, k) + end, filter(function (k, elt) return elt.working end, mxes)) + task:insert_result(settings.symbol_good_mx, 1.0, valid_mx) + local ret,_,_ = rspamd_redis_make_request(task, + redis_params, -- connect params + key, -- hash key + false, -- is write + redis_cache_cb, --callback + 'SETEX', -- command + {key, tostring(settings.expire), table.concat(valid_mx, ';')} -- arguments + ) + end + end + end + + local function gen_mx_a_callback(name, mxes) + return function(resolver, to_resolve, results, err, _, authenticated) + mxes[name].ips = results + + local function io_cb(err, data, conn) + if err then + mxes[name].checked = true + else + mxes[name].checked = true + mxes[name].working = true + valid = true + end + check_results(mxes) + end + local function on_connect_cb(conn) + if err then + mxes[name].checked = true + else + mxes[name].checked = true + valid = true + mxes[name].working = true + end + conn:close() + check_results(mxes) + end + + if err or not results then + mxes[name].checked = true + else + -- Try to open TCP connection to port 25 + for _,res in ipairs(results) do + local ret = rspamd_tcp.new({ + task = task, + host = res:to_string(), + callback = io_cb, + on_connect = on_connect_cb, + timeout = settings.timeout, + port = 25 + }) + + if not ret then + mxes[name].checked = true + end + end + end + check_results(mxes) + end + end + + local function mx_callback(resolver, to_resolve, results, err, _, authenticated) + if err or not results then + task:insert_result(settings.symbol_no_mx, 1.0) + else + local mxes = {} + table.sort(results, function(r1, r2) + return r1['priority'] < r2['priority'] + end) + for _,mx in ipairs(results) do + -- Not checked + mxes[mx['name']] = {checked = false, working = false, ips = {}} + end + + for _,mx in ipairs(results) do + local r = task:get_resolver() + -- XXX: maybe add ipv6? + r:resolve('a', { + name = mx['name'], + callback = gen_mx_a_callback(mx['name'], mxes), + task = task, + forced = true + }) + end + check_results(mxes) + end + end + + if not redis_params then + local r = task:get_resolver() + r:resolve('mx', { + name = mx_domain, + callback = mx_callback, + task = task, + forced = true + }) + else + local function redis_cache_get_cb(task, err, data) + if err or type(data) ~= 'string' then + local r = task:get_resolver() + r:resolve('mx', { + name = mx_domain, + callback = mx_callback, + task = task, + forced = true + }) + else + if data == '0' then + task:insert_result(settings.symbol_bad_mx, 1.0, 'cached') + else + task:insert_result(settings.symbol_good_mx, 1.0, {'cached', data}) + end + end + end + + local key = settings.key_prefix .. mx_domain + local ret,_,_ = rspamd_redis_make_request(task, + redis_params, -- connect params + key, -- hash key + false, -- is write + redis_cache_get_cb, --callback + 'GET', -- command + {key} -- arguments + ) + + if not ret then + local r = task:get_resolver() + r:resolve('mx', { + name = mx_domain, + callback = mx_callback, + task = task, + forced = true + }) + end + end +end + +-- Module setup +local opts = rspamd_config:get_all_opt('mx_check') +if not (opts and type(opts) == 'table') then + rspamd_logger.infox(rspamd_config, 'module is unconfigured') + return +end +if opts then + redis_params = rspamd_parse_redis_server('mx_check') + for k,v in pairs(opts) do + settings[k] = v + end + + local id = rspamd_config:register_symbol({ + name = settings.symbol_bad_mx, + type = 'normal', + callback = mx_check, + }) + rspamd_config:register_symbol({ + name = settings.symbol_no_mx, + type = 'virtual', + parent = id + }) + rspamd_config:register_symbol({ + name = settings.symbol_good_mx, + type = 'virtual', + parent = id + }) + + rspamd_config:set_metric_symbol({ + name = settings.symbol_bad_mx, + score = 4.0, + description = 'Domain has no working MX', + group = 'MX' + }) + rspamd_config:set_metric_symbol({ + name = settings.symbol_good_mx, + score = -0.1, + description = 'Domain has working MX', + group = 'MX' + }) + rspamd_config:set_metric_symbol({ + name = settings.symbol_no_mx, + score = 1.5, + description = 'Domain has no working MX', + group = 'MX' + }) +end