]> source.dussan.org Git - rspamd.git/commitdiff
New rules 599/head
authorSteve Freegard <steve@stevefreegard.com>
Fri, 22 Apr 2016 11:20:21 +0000 (12:20 +0100)
committerSteve Freegard <steve@stevefreegard.com>
Fri, 22 Apr 2016 11:20:21 +0000 (12:20 +0100)
conf/composites.conf
rules/forwarding.lua [new file with mode: 0644]
rules/misc.lua
rules/regexp/upstream_spam_filters.lua [new file with mode: 0644]
rules/rspamd.lua

index bfa0b1b47492201d980e7f95afa839678a767f58..b3372a997cee231174429766b8485197d914132e 100644 (file)
@@ -22,6 +22,14 @@ composite {
     name = "FORGED_SENDER_MAILLIST";
     expression = "FORGED_SENDER & -MAILLIST";
 }
+composite {
+    name = "FORGED_SENDER_FORWARDING";
+    expression = "FORGED_SENDER & g:forwarding";
+}
+composite {
+    name = "FORGED_SENDER_VERP_SRS";
+    expression = "FORGED_SENDER & (ENVFROM_PRVS | ENVFROM_VERP)";
+}
 composite {
     name = "FORGED_MUA_MAILLIST";
     expression = "g:mua and -MAILLIST";
@@ -32,4 +40,4 @@ composite {
 }
 
 .include(try=true; priority=1) "$LOCAL_CONFDIR/local.d/composites.conf"
-.include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/composites.conf"
\ No newline at end of file
+.include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/composites.conf"
diff --git a/rules/forwarding.lua b/rules/forwarding.lua
new file mode 100644 (file)
index 0000000..6ee0b9a
--- /dev/null
@@ -0,0 +1,109 @@
+--[[
+Copyright (c) 2011-2016, Vsevolod Stakhov <vsevolod@highsecure.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.
+]]--
+
+-- Rules to detect forwarding
+
+rspamd_config.FWD_GOOGLE = {
+    callback = function (task)
+        if not (task:has_from(1) and task:has_recipients(1)) then
+            return false
+        end
+        local envfrom = task:get_from(1)
+        local envrcpts = task:get_recipients(1)
+        -- Forwarding will only be to a single recipient
+        if table.getn(envrcpts) > 1 then return false end
+        -- Get recipient and compute VERP address
+        local rcpt = envrcpts[1].addr:lower()
+        local verp = rcpt:gsub('@','=')
+        -- Get the user portion of the envfrom
+        local ef_user = envfrom[1].user:lower()
+        -- Check for a match
+        if ef_user:find('+caf_=' .. verp, 1, true) then
+            local _,_,user = ef_user:find('^(.+)+caf_=')
+            if user then
+                user = user .. '@' .. envfrom[1].domain
+                return true, user
+            end
+        end
+        return false
+    end,
+    score = 0.1,
+    description = "Message was forwarded by Google",
+    group = "forwarding"
+}
+
+rspamd_config.FWD_SRS = {
+    callback = function (task)
+        if not (task:has_from(1) and task:has_recipients(1)) then
+            return false
+        end
+        local envfrom = task:get_from(1)
+        local envrcpts = task:get_recipients(1)
+        -- Forwarding is only to a single recipient
+        if table.getn(envrcpts) > 1 then return false end
+        -- Get recipient and compute rewritten SRS address
+        local srs = '=' .. envrcpts[1].domain:lower() ..
+                    '=' .. envrcpts[1].user:lower()
+        if envfrom[1].user:lower():find('^srs[01]=') and
+           envfrom[1].user:lower():find(srs, 1, false)
+        then
+            return true
+        end
+        return false
+    end,
+    score = 0.1,
+    description = "Message was forwarded using SRS",
+    group = "forwarding"
+}
+
+rspamd_config.FORWARDED = {
+    callback = function (task)
+        if not task:has_recipients(1) then return false end
+        local envrcpts = task:get_recipients(1)
+        -- Forwarding will only be for single recipient messages
+        if table.getn(envrcpts) > 1 then return false end
+        -- Get any other headers we might need
+        local lu = task:get_header('List-Unsubscribe')
+        local to = task:get_recipients(2)
+        local matches = 0
+        -- Retrieve and loop through all Received headers
+        local rcvds = task:get_header_full('Received')
+        for _, rcvd in ipairs(rcvds) do
+            local _,_,addr = rcvd['decoded']:lower():find("%sfor%s<(.-)>")
+            if addr then
+                matches = matches + 1
+                -- Check that it doesn't match the envrcpt
+                -- TODO: remove any plus addressing?
+                if addr ~= envrcpts[1].addr:lower() then
+                    -- Check for mailing-lists as they will have the same signature
+                    if matches < 2 and lu and to and to[1].addr:lower() == addr then
+                        return false
+                    else
+                        return true, addr
+                    end
+                end
+                -- Prevent any other iterations as we only want
+                -- process the first matching Received header
+                return false
+            end
+        end
+        return false
+    end,
+    score = 0.1,
+    description = "Message was forwarded",
+    group = "forwarding"
+}
+
index b3926e46b707c03204da6fdeeb601472d93b57cc..f72efea87ec66e990dcb3bf80d150086ebcdd181 100644 (file)
@@ -237,3 +237,77 @@ rspamd_config.MULTIPLE_UNIQUE_HEADERS = {
   group = 'headers',
   description = 'Repeated unique headers'
 }
+
+rspamd_config.ENVFROM_PRVS = {
+    callback = function (task)
+        -- Detect PRVS/BATV addresses to avoid FORGED_SENDER
+        -- https://en.wikipedia.org/wiki/Bounce_Address_Tag_Validation
+        if not (task:has_from(1) and task:has_from(2)) then
+            return false
+        end
+        local envfrom = task:get_from(1)
+        local tag,ef = envfrom[1].addr:lower():match("^prvs=([^=]+)=(.+)$")
+        if not ef then return false end
+        -- See if it matches the From header
+        local from = task:get_from(2)
+        if ef == from[1].addr:lower() then
+            return true
+        end
+        return false
+    end,
+    score = 0.01,
+    description = "Envelope From is a PRVS address that matches the From address",
+    group = 'prvs'
+}
+
+rspamd_config.ENVFROM_VERP = {
+    callback = function (task)
+        if not (task:has_from(1) and task:has_recipients(1)) then
+            return false
+        end
+        local envfrom = task:get_from(1)
+        local envrcpts = task:get_recipients(1)
+        -- VERP only works for single recipient messages
+        if table.getn(envrcpts) > 1 then return false end
+        -- Get recipient and compute VERP address
+        local rcpt = envrcpts[1].addr:lower()
+        local verp = rcpt:gsub('@','=')
+        -- Get the user portion of the envfrom
+        local ef_user = envfrom[1].user:lower()
+        -- See if the VERP representation of the recipient appears in it
+        if ef_user:find(verp, 1, true)
+           and not ef_user:find('+caf_=' .. verp, 1, true) -- Google Forwarding
+           and not ef_user:find('^srs[01]=')               -- SRS
+        then
+            return true
+        end
+        return false
+    end,
+    score = 0.01,
+    description = "Envelope From is a VERP address",
+    group = "mailing_list"
+}
+
+rspamd_config.RCVD_TLS_ALL = {
+    callback = function (task)
+        local rcvds = task:get_header_full('Received')
+        local count = 0
+        local encrypted = 0
+        for _, rcvd in ipairs(rcvds) do
+            count = count + 1
+            local r = rcvd['decoded']:lower()
+            local by = r:match('^by%s+([^%s]+)') or r:match('%sby%s+([^%s]+)')
+            local with = r:match('%swith%s+(e?smtps?a?)')
+            if with and with:match('esmtps') then
+                encrypted = encrypted + 1
+            end
+        end
+        if (count > 0 and count == encrypted) then
+            return true
+        end
+    end,
+    score = 0.01,
+    description = "All hops used encrypted transports",
+    group = "encryption"
+}
+
diff --git a/rules/regexp/upstream_spam_filters.lua b/rules/regexp/upstream_spam_filters.lua
new file mode 100644 (file)
index 0000000..066b8fd
--- /dev/null
@@ -0,0 +1,46 @@
+--[[
+Copyright (c) 2011-2016, Vsevolod Stakhov <vsevolod@highsecure.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.
+]]--
+
+-- Rules for upstream services that have already run spam checks
+
+reconf['PRECEDENCE_BULK'] = {
+    re = 'Precedence=/bulk/Hi',
+    score = 3,
+    description = "Message marked as bulk",
+    group = 'upstream_spam_filters'
+}
+
+reconf['MICROSOFT_SPAM'] = {
+    -- https://technet.microsoft.com/en-us/library/dn205071(v=exchg.150).aspx
+    re = 'X-Forefront-Antispam-Report=/SFV:SPM/H',
+    score = 10,
+    description = "Microsoft says the messge is spam",
+    group = 'upstream_spam_filters'
+}
+
+reconf['AOL_SPAM'] = {
+    re = 'X-AOL-Global-Disposition=/^S/H',
+    score = 5,
+    description = "AOL says this message is spam",
+    group = 'upstream_spam_filters'
+}
+
+reconf['SPAM_FLAG'] = {
+    re = 'X-Spam-Flag=/^(?:yes|true)/Hi',
+    score = 5,
+    description = "Message was already marked as spam",
+    group = 'upstream_spam_filters'
+}
index 4c6ce61d90da53d3115ad944969e5b034732be7d..a3cdc919be8b23417f46dd414dd99e4b5b6ea41e 100644 (file)
@@ -28,9 +28,11 @@ dofile(local_rules .. '/regexp/headers.lua')
 dofile(local_rules .. '/regexp/lotto.lua')
 dofile(local_rules .. '/regexp/fraud.lua')
 dofile(local_rules .. '/regexp/drugs.lua')
+dofile(local_rules .. '/regexp/upstream_spam_filters.lua')
 dofile(local_rules .. '/html.lua')
 dofile(local_rules .. '/misc.lua')
 dofile(local_rules .. '/http_headers.lua')
+dofile(local_rules .. '/forwarding.lua')
 
 local function file_exists(filename)
        local file = io.open(filename)