]> source.dussan.org Git - rspamd.git/commitdiff
[Feature] Add BOUNCE rule 3468/head
authorAnton Yuzhaninov <citrin+git@citrin.ru>
Mon, 10 Aug 2020 15:04:10 +0000 (16:04 +0100)
committerAnton Yuzhaninov <citrin+git@citrin.ru>
Mon, 10 Aug 2020 18:27:16 +0000 (19:27 +0100)
conf/scores.d/headers_group.conf
rules/bounce.lua [new file with mode: 0644]
rules/rspamd.lua

index c82c3a7529cbc9a8bb9405be3805fd3b4a3108e8..83048ea28606f25ed357ed93d3fa81d4a621a9b6 100644 (file)
@@ -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 (file)
index 0000000..21c0d3f
--- /dev/null
@@ -0,0 +1,117 @@
+--[[
+Copyright (c) 2020, Anton Yuzhaninov <citrin@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')
index a5dbef42df9430ba6e22d38e5eb861e352e46014..64aefa9d19d1b821281d0f53ae0a98098eb4cc33 100644 (file)
@@ -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