From: Anton Yuzhaninov Date: Mon, 10 Aug 2020 15:04:10 +0000 (+0100) Subject: [Feature] Add BOUNCE rule X-Git-Tag: 2.6~161^2 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=refs%2Fpull%2F3468%2Fhead;p=rspamd.git [Feature] Add BOUNCE rule --- diff --git a/conf/scores.d/headers_group.conf b/conf/scores.d/headers_group.conf index c82c3a752..83048ea28 100644 --- a/conf/scores.d/headers_group.conf +++ b/conf/scores.d/headers_group.conf @@ -68,4 +68,8 @@ symbols = { weight = -0.2; description = "Message seems to be from maillist"; } + "BOUNCE" { + weight = -0.1; + description = "(Non) Delivery Status Notification"; + } } diff --git a/rules/bounce.lua b/rules/bounce.lua new file mode 100644 index 000000000..21c0d3fe0 --- /dev/null +++ b/rules/bounce.lua @@ -0,0 +1,117 @@ +--[[ +Copyright (c) 2020, Anton Yuzhaninov + +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. +]]-- + +-- Rule to detect bounces: +-- RFC 3464 Delivery status notifications and most common non-standard ones + +local function make_subj_bounce_keywords_re() + -- Words and phrases commonly used in Subjects for bounces + -- We cannot practically test all localized Subjects, but luckily English is by far the most common here + local keywords = { + 'could not send message', + "couldn't be delivered", + 'delivery failed', + 'delivery failure', + 'delivery report', + 'delivery warning', + 'failure delivery', + 'failure notice', + "hasn't been delivered", + 'mail failure', + 'returned mail', + 'undeliverable', + 'undelivered', + } + return string.format([[Subject=/\b(%s)\b/i{header}]], table.concat(keywords, '|')) +end + +config.regexp.SUBJ_BOUNCE_WORDS = { + re = make_subj_bounce_keywords_re(), + group = 'headers', + score = 0.0, + description = 'Words/phrases typical for DNS' +} + +rspamd_config.BOUNCE = { + callback = function(task) + local from = task:get_from('smtp') + if from and from[1].addr ~= '' then + -- RFC 3464: + -- Whenever an SMTP transaction is used to send a DSN, the MAIL FROM + -- command MUST use a NULL return address, i.e., "MAIL FROM:<>" + -- In practise it is almost always the case for DNS + return false + end + + + local parts = task:get_parts() + local top_type, top_subtype, params = parts[1]:get_type_full() + -- RFC 3464, RFC 8098 + if top_type == 'multipart' and top_subtype == 'report' and params and + (params['report-type'] == 'delivery-status' or params['report-type'] == 'disposition-notification') then + -- Assume that inner parts are OK, don't check them to save time + return true, 1.0, 'DSN' + end + + -- Apply heuristics for non-standard bounecs + local bounce_sender + local mime_from = task:get_from('mime') + if mime_from then + local from_user = mime_from[1].user:lower() + -- Check common bounce senders + if (from_user == 'postmaster' or from_user == 'mailer-daemon') then + bounce_sender = from_user + -- MDaemon >= 14.5 sends multipart/report (RFC 3464) DNS covered above, + -- but older versions send non-standard bounces with localized subjects and they + -- are still around + elseif from_user == 'mdaemon' and task:has_header('X-MDDSN-Message') then + return true, 1.0, 'MDaemon' + end + end + + local subj_keywords = task:has_symbol('SUBJ_BOUNCE_WORDS') + + if not (bounce_sender or subj_keywords) then + return false + end + + if bounce_sender and subj_keywords then + return true, 0.5, bounce_sender .. '+subj' + end + + -- Look for a message/rfc822(-headers) part inside + local rfc822_part + parts[10] = nil -- limit numbe of parts to check + for _, p in ipairs(parts) do + local mime_type, mime_subtype = p:get_type() + if (mime_subtype == 'rfc822' or mime_subtype == 'rfc822-headers') and + (mime_type == 'message' or mime_type == 'text') then + rfc822_part = mime_type .. '/' .. mime_subtype + break + end + end + + if rfc822_part and bounce_sender then + return true, 0.5, bounce_sender .. '+' .. rfc822_part + elseif rfc822_part and subj_keywords then + return true, 0.2, rfc822_part .. '+subj' + end + end, + description = '(Non) Delivery Status Notification', + group = 'headers', +} + +rspamd_config:register_dependency('BOUNCE', 'SUBJ_BOUNCE_WORDS') diff --git a/rules/rspamd.lua b/rules/rspamd.lua index a5dbef42d..64aefa9d1 100644 --- a/rules/rspamd.lua +++ b/rules/rspamd.lua @@ -37,6 +37,7 @@ dofile(local_rules .. '/http_headers.lua') dofile(local_rules .. '/forwarding.lua') dofile(local_rules .. '/mid.lua') dofile(local_rules .. '/bitcoin.lua') +dofile(local_rules .. '/bounce.lua') dofile(local_rules .. '/content.lua') dofile(local_rules .. '/controller/init.lua') @@ -65,4 +66,4 @@ if rmaps and type(rmaps) == 'table' then rspamd_maps[k] = map_or_err end end -end \ No newline at end of file +end