commit 228161c: [Feature] Add BOUNCE rule

Anton Yuzhaninov citrin+git at citrin.ru
Mon Aug 10 19:28:06 UTC 2020


Author: Anton Yuzhaninov
Date: 2020-08-10 16:04:10 +0100
URL: https://github.com/rspamd/rspamd/commit/228161c45cb2443ff942dd5886a82e43862fa9d3 (refs/pull/3468/head)

[Feature] Add BOUNCE rule

---
 conf/scores.d/headers_group.conf |   4 ++
 rules/bounce.lua                 | 117 +++++++++++++++++++++++++++++++++++++++
 rules/rspamd.lua                 |   3 +-
 3 files changed, 123 insertions(+), 1 deletion(-)

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 <citrin at citrin.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.
+]]--
+
+-- 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


More information about the Commits mailing list