aboutsummaryrefslogtreecommitdiffstats
path: root/rules/misc.lua
diff options
context:
space:
mode:
Diffstat (limited to 'rules/misc.lua')
-rw-r--r--rules/misc.lua582
1 files changed, 582 insertions, 0 deletions
diff --git a/rules/misc.lua b/rules/misc.lua
index 89ba97fb1..9e30fe23f 100644
--- a/rules/misc.lua
+++ b/rules/misc.lua
@@ -395,3 +395,585 @@ rspamd_config.MISSING_FROM = {
group = 'header',
description = 'Missing From: header'
}
+
+rspamd_config.RCVD_HELO_USER = {
+ callback = function (task)
+ -- Check HELO argument from MTA
+ local helo = task:get_helo()
+ if (helo and helo:lower():find('^user$')) then
+ return true
+ end
+ -- Check Received headers
+ local rcvds = task:get_header_full('Received')
+ if not rcvds then return false end
+ for _, rcvd in ipairs(rcvds) do
+ local r = rcvd['decoded']:lower()
+ if (r:find("^%s*from%suser%s")) then return true end
+ if (r:find("helo[%s=]user[%s%)]")) then return true end
+ end
+ end,
+ description = 'HELO User spam pattern',
+ score = 3.0
+}
+
+rspamd_config.URI_COUNT_ODD = {
+ callback = function (task)
+ local ct = task:get_header('Content-Type')
+ if (ct and ct:lower():find('^multipart/alternative')) then
+ local urls = task:get_urls()
+ if (urls and (#urls % 2 == 1)) then
+ return true
+ end
+ end
+ end,
+ description = 'Odd number of URIs in multipart/alternative message',
+ score = 1.0
+}
+
+rspamd_config.HAS_ATTACHMENT = {
+ callback = function (task)
+ local parts = task:get_parts()
+ if parts and #parts > 1 then
+ for _, p in ipairs(parts) do
+ local cd = p:get_header('Content-Disposition')
+ if (cd and cd:lower():match('^attachment')) then
+ return true
+ end
+ end
+ end
+ end,
+ description = 'Message contains attachments'
+}
+
+rspamd_config.MV_CASE = {
+ callback = function (task)
+ local mv = task:get_header('Mime-Version', true)
+ if (mv) then return true end
+ end,
+ description = 'Mime-Version .vs. MIME-Version',
+ score = 0.5
+}
+
+rspamd_config.FAKE_REPLY = {
+ callback = function (task)
+ local subject = task:get_header('Subject')
+ if (subject and subject:lower():find('^re:')) then
+ local ref = task:get_header('References')
+ local rt = task:get_header('In-Reply-To')
+ if (not (ref or rt)) then return true end
+ end
+ return false
+ end,
+ description = 'Fake reply',
+ score = 1.0
+}
+
+rspamd_config.CHECK_FROM = {
+ callback = function(task)
+ local envfrom = task:get_from(1)
+ local from = task:get_from(2)
+ if (from and from[1] and not from[1].name) then
+ task:insert_result('FROM_NO_DN', 1.0)
+ elseif (from and from[1] and from[1].name and
+ from[1].name:lower() == from[1].addr:lower()) then
+ task:insert_result('FROM_DN_EQ_ADDR', 1.0)
+ elseif (from and from[1] and from[1].name) then
+ task:insert_result('FROM_HAS_DN', 1.0)
+ -- Look for Mr/Mrs/Dr titles
+ local n = from[1].name:lower()
+ if (n:find('^mrs?[%.%s]') or n:find('^dr[%.%s]')) then
+ task:insert_result('FROM_NAME_HAS_TITLE', 1.0)
+ end
+ end
+ if (envfrom and from and envfrom[1] and from[1] and
+ envfrom[1].addr:lower() == from[1].addr:lower())
+ then
+ task:insert_result('FROM_EQ_ENVFROM', 1.0)
+ elseif (envfrom and envfrom[1] and envfrom[1].addr) then
+ task:insert_result('FROM_NEQ_ENVFROM', 1.0, from[1].addr, envfrom[1].addr)
+ end
+
+ local to = task:get_recipients(2)
+ if not (to and to[1]) then return false end
+ -- Check if FROM == TO
+ if (#to == 1 and to[1].addr:lower() == from[1].addr:lower()) then
+ task:insert_result('TO_EQ_FROM', 1.0)
+ elseif (#to == 1 and to[1].domain:lower() == from[1].domain:lower()) then
+ task:insert_result('TO_DOM_EQ_FROM_DOM', 1.0)
+ end
+ end
+}
+
+rspamd_config.FROM_NO_DN = {
+ callback = function()
+ -- Set by CHECK_FROM
+ end,
+ description = 'From header does not have a display name',
+ score = 0.0
+}
+
+rspamd_config.FROM_DN_EQ_ADDR = {
+ callback = function()
+ -- Set by CHECK_FROM
+ end,
+ description = 'From header display name is the same as the address',
+ score = 1.0
+}
+
+rspamd_config.FROM_HAS_DN = {
+ callback = function()
+ -- Set by CHECK_FROM
+ end,
+ description = 'From header has a display name',
+ score = 0.0
+}
+
+rspamd_config.FROM_NAME_HAS_TITLE = {
+ callback = function()
+ -- Set by CHECK_FROM
+ end,
+ description = 'From header display name has a title (Mr/Mrs/Dr)',
+ score = 1.0
+}
+
+rspamd_config.FROM_EQ_ENVFROM = {
+ callback = function()
+ -- Set by CHECK_FROM
+ end,
+ description = 'From address is the same as the envelope',
+ score = 0.0
+}
+
+rspamd_config.FROM_NEQ_ENVFROM = {
+ callback = function()
+ -- Set by CHECK_FROM
+ end,
+ description = 'From address is different to the envelope',
+ score = 0.0
+}
+
+rspamd_config.TO_EQ_FROM = {
+ callback = function()
+ -- Set by CHECK_FROM
+ end,
+ description = 'To address matches the From address',
+ score = 0.0
+}
+
+rspamd_config.TO_DOM_EQ_FROM_DOM = {
+ callback = function()
+ -- Set by CHECK_FROM
+ end,
+ description = 'To domain is the same as the From domain',
+ score = 0.0
+}
+
+
+rspamd_config.CHECK_TO_CC = {
+ callback = function(task)
+ local rcpts = task:get_recipients(1)
+ local to = task:get_recipients(2)
+ local to_match_envrcpt = 0
+ if (not to) then return false end
+ -- Add symbol for recipient count
+ if (#to > 50) then
+ task:insert_result('RCPT_COUNT_GT_50', 1.0)
+ else
+ task:insert_result('RCPT_COUNT_' .. #to, 1.0)
+ end
+ -- Check for display names
+ local to_dn_count = 0
+ local to_dn_eq_addr_count = 0
+ for _, toa in ipairs(to) do
+ -- To: Recipients <noreply@dropbox.com>
+ if (toa['name'] and (toa['name']:lower() == 'recipient'
+ or toa['name']:lower() == 'recipients')) then
+ task:insert_result('TO_DN_RECIPIENTS', 1.0)
+ end
+ if (toa['name'] and toa['name']:lower() == toa['addr']:lower()) then
+ to_dn_eq_addr_count = to_dn_eq_addr_count + 1
+ elseif (toa['name']) then
+ to_dn_count = to_dn_count + 1
+ end
+ -- See if header recipients match envrcpts
+ if (rcpts) then
+ for _, rcpt in ipairs(rcpts) do
+ if (toa and toa['addr'] and rcpt and rcpt['addr'] and
+ rcpt['addr']:lower() == toa['addr']:lower())
+ then
+ to_match_envrcpt = to_match_envrcpt + 1
+ end
+ end
+ end
+ end
+ if (to_dn_count == 0 and to_dn_eq_addr_count == 0) then
+ task:insert_result('TO_DN_NONE', 1.0)
+ elseif (to_dn_count == #to) then
+ task:insert_result('TO_DN_ALL', 1.0)
+ elseif (to_dn_count > 0) then
+ task:insert_result('TO_DN_SOME', 1.0)
+ end
+ if (to_dn_eq_addr_count == #to) then
+ task:insert_result('TO_DN_EQ_ADDR_ALL', 1.0)
+ elseif (to_dn_eq_addr_count > 0) then
+ task:insert_result('TO_DN_EQ_ADDR_SOME', 1.0)
+ end
+
+ -- See if header recipients match envelope recipients
+ if (to_match_envrcpt == #to) then
+ task:insert_result('TO_MATCH_ENVRCPT_ALL', 1.0)
+ elseif (to_match_envrcpt > 0) then
+ task:insert_result('TO_MATCH_ENVRCPT_SOME', 1.0)
+ end
+ end
+}
+
+rspamd_config.TO_DN_RECIPIENTS = {
+ callback = function()
+ -- Set by CHECK_TO_CC
+ end,
+ description = 'To header display name is "Recipients"',
+ score = 2.0
+}
+
+rspamd_config.TO_DN_NONE = {
+ callback = function()
+ -- Set by CHECK_TO_CC
+ end,
+ description = 'None of the recipients have display names',
+ score = 0.0
+}
+
+rspamd_config.TO_DN_ALL = {
+ callback = function()
+ -- Set by CHECK_TO_CC
+ end,
+ description = 'All of the recipients have display names',
+ score = 0.0
+}
+
+rspamd_config.TO_DN_SOME = {
+ callback = function()
+ -- Set by CHECK_TO_CC
+ end,
+ description = 'Some of the recipients have display names',
+ score = 0.0
+}
+
+rspamd_config.TO_DN_EQ_ADDR_ALL = {
+ callback = function()
+ -- Set by CHECK_TO_CC
+ end,
+ description = 'All of the recipients have display names that are the same as their address',
+ score = 0.0
+}
+
+rspamd_config.TO_DN_EQ_ADDR_SOME = {
+ callback = function()
+ -- Set by CHECK_TO_CC
+ end,
+ description = 'Some of the recipients have display names that are the same as their address',
+ score = 0.0
+}
+
+rspamd_config.TO_MATCH_ENVRCPT_ALL = {
+ callback = function()
+ -- Set by CHECK_TO_CC
+ end,
+ description = 'All of the recipients match the envelope',
+ score = 0.0
+}
+
+rspamd_config.TO_MATCH_ENVRCPT_SOME = {
+ callback = function()
+ -- Set by CHECK_TO_CC
+ end,
+ description = 'Some of the recipients match the envelope',
+ score = 0.0
+}
+
+
+rspamd_config.CHECK_MID = {
+ callback = function (task)
+ local mid = task:get_header('Message-ID')
+ if not mid then return false end
+ -- Check for 'bare' IP addresses in RHS
+ if mid:find("@%d+%.%d+%.%d+%.%d+>$") then
+ task:insert_result('MID_BARE_IP', 1.0)
+ end
+ -- Check for non-FQDN RHS
+ if mid:find("@[^%.]+>?$") then
+ task:insert_result('MID_RHS_NOT_FQDN', 1.0)
+ end
+ -- Check for missing <>'s
+ if not mid:find('^<[^>]+>$') then
+ task:insert_result('MID_MISSING_BRACKETS', 1.0)
+ end
+ -- Check for IP literal in RHS
+ if mid:find("@%[%d+%.%d+%.%d+%.%d+%]") then
+ task:insert_result('MID_RHS_IP_LITERAL', 1.0)
+ end
+ -- Check From address atrributes against MID
+ local from = task:get_from(2)
+ if (from and from[1] and from[1].domain) then
+ local fd = from[1].domain:lower()
+ local _,_,md = mid:find("@([^>]+)>?$")
+ -- See if all or part of the From address
+ -- can be found in the Message-ID
+ if (mid:lower():find(from[1].addr:lower(),1,true)) then
+ task:insert_result('MID_CONTAINS_FROM', 1.0)
+ elseif (md and fd == md:lower()) then
+ task:insert_result('MID_RHS_MATCH_FROM', 1.0)
+ end
+ end
+ end
+}
+
+
+rspamd_config.MID_BARE_IP = {
+ callback = function()
+ -- Set by CHECK_MID
+ end,
+ description = 'Message-ID RHS is a bare IP address',
+ score = 2.0
+}
+
+rspamd_config.MID_RHS_NOT_FQDN = {
+ callback = function()
+ -- Set by CHECK_MID
+ end,
+ description = 'Message-ID RHS is not a fully-qualified domain name',
+ score = 0.5
+}
+
+rspamd_config.MID_MISSING_BRACKETS = {
+ callback = function()
+ -- Set by CHECK_MID
+ end,
+ description = 'Message-ID is missing <>\'s',
+ score = 0.5
+}
+
+rspamd_config.MID_RHS_IP_LITERAL = {
+ callback = function ()
+ -- Set by CHECK_MID
+ end,
+ description = 'Message-ID RHS is an IP-literal',
+ score = 0.5
+}
+
+rspamd_config.MID_CONTAINS_FROM = {
+ callback = function ()
+ -- Set by CHECK_MID
+ end,
+ description = 'Message-ID contains From address',
+ score = 1.0
+}
+
+rspamd_config.MID_RHS_MATCH_FROM = {
+ callback = function ()
+ -- Set by CHECK_MID
+ end,
+ description = 'Message-ID RHS matches From domain',
+ score = 1.0
+}
+
+rspamd_config.CHECK_RECEIVED = {
+ callback = function (task)
+ local received = task:get_received_headers()
+ task:insert_result('RCVD_COUNT_' .. #received, 1.0)
+ end
+}
+
+rspamd_config.HAS_X_PRIO = {
+ callback = function (task)
+ local xprio = task:get_header('X-Priority');
+ if not xprio then return false end
+ local _,_,x = xprio:find('^%s?(%d+)');
+ if (x) then
+ task:insert_result('HAS_X_PRIO_' .. x, 1.0)
+ end
+ end
+}
+
+rspamd_config.CHECK_REPLYTO = {
+ callback = function (task)
+ local replyto = task:get_header('Reply-To')
+ if not replyto then return false end
+ local rt = util.parse_mail_address(replyto)
+ if not (rt and rt[1]) then
+ task:insert_result('REPLYTO_UNPARSEABLE', 1.0)
+ return false
+ else
+ task:insert_result('HAS_REPLYTO', 1.0)
+ end
+
+ -- See if Reply-To matches From in some way
+ local from = task:get_from(2)
+ local from_h = task:get_header('From')
+ if not (from and from[1]) then return false end
+ if (from_h and from_h == replyto) then
+ -- From and Reply-To are identical
+ task:insert_result('REPLYTO_EQ_FROM', 1.0)
+ else
+ if (from and from[1]) then
+ -- See if From and Reply-To addresses match
+ if (from[1].addr:lower() == rt[1].addr:lower()) then
+ task:insert_result('REPLYTO_ADDR_EQ_FROM', 1.0)
+ elseif (from[1].domain:lower() == rt[1].addr:lower()) then
+ task:insert_result('REPLYTO_DOM_EQ_FROM_DOM', 1.0)
+ elseif (from[1].domain:lower() ~= rt[1].domain:lower()) then
+ task:insert_result('REPLYTO_DOM_NEQ_FROM_DOM', 1.0)
+ end
+ -- See if the Display Names match
+ if (from[1].name and rt[1].name and from[1].name:lower() == rt[1].name:lower()) then
+ task:insert_result('REPLYTO_DN_EQ_FROM_DN', 1.0)
+ end
+ end
+ end
+ end
+}
+
+rspamd_config.REPLYTO_UNPARSEABLE = {
+ callback = function ()
+ -- Set by CHECK_REPLYTO
+ end,
+ description = 'Reply-To header could not be parsed',
+ score = 1.0
+}
+
+rspamd_config.HAS_REPLYTO = {
+ callback = function ()
+ -- Set by CHECK_REPLYTO
+ end,
+ description = 'Has Reply-To header',
+ score = 0.0
+}
+
+rspamd_config.REPLYTO_EQ_FROM = {
+ callback = function ()
+ -- Set by CHECK_REPLYTO
+ end,
+ description = 'Reply-To header is identical to From header',
+ score = 0.0
+}
+
+rspamd_config.REPLYTO_ADDR_EQ_FROM = {
+ callback = function ()
+ -- Set by CHECK_REPLYTO
+ end,
+ description = 'Reply-To address is the same as From',
+ score = 0.0
+}
+
+rspamd_config.REPLYTO_DOM_EQ_FROM_DOM = {
+ callback = function ()
+ -- Set by CHECK_REPLYTO
+ end,
+ description = 'Reply-To domain matches the From domain',
+ score = 0.0
+}
+
+rspamd_config.REPLYTO_DOM_NEQ_FROM_DOM = {
+ callback = function ()
+ -- Set by CHECK_REPLYTO
+ end,
+ description = 'Reply-To domain does not match the From domain',
+ score = 0.0
+}
+
+rspamd_config.REPLYTO_DN_EQ_FROM_DN = {
+ callback = function ()
+ -- Set by CHECK_REPLYTO
+ end,
+ description = 'Reply-To display name matches From',
+ score = 0.0
+}
+
+rspamd_config.CHECK_MIME = {
+ callback = function (task)
+ local parts = task:get_parts()
+ if not parts then return false end
+
+ -- Make sure there is a MIME-Version header
+ local mv = task:get_header('MIME-Version')
+ if (not mv) then
+ task:insert_result('MISSING_MIME_VERSION', 1.0)
+ end
+
+ local found_ma = false
+ local found_plain = false
+ local found_html = false
+
+ for _,p in ipairs(parts) do
+ local mtype,subtype = p:get_type()
+ local ctype = mtype:lower() .. '/' .. subtype:lower()
+ if (ctype == 'multipart/alternative') then
+ found_ma = true
+ end
+ if (ctype == 'text/plain') then
+ found_plain = true
+ end
+ if (ctype == 'text/html') then
+ found_html = true
+ end
+ end
+
+ if (found_ma) then
+ if (not found_plain) then
+ task:insert_result('MIME_MA_MISSING_TEXT', 1.0)
+ end
+ if (not found_html) then
+ task:insert_result('MIME_MA_MISSING_HTML', 1.0)
+ end
+ end
+ end
+}
+
+rspamd_config.MISSING_MIME_VERSION = {
+ callback = function ()
+ -- Set by CHECK_MIME
+ end,
+ description = 'MIME-Version header is missing',
+ score = 2.0
+}
+
+rspamd_config.MIME_MA_MISSING_TEXT = {
+ callback = function ()
+ -- Set by CHECK_MIME
+ end,
+ description = 'MIME multipart/alternative missing text/plain part',
+ score = 2.0
+}
+
+rspamd_config.MIME_NA_MISSING_HTML = {
+ callback = function ()
+ -- Set by CHECK_MIME
+ end,
+ description = 'MIME multipart/alternative missing text/html part',
+ score = 2.0
+}
+
+-- Used to be called IS_LIST
+rspamd_config.PREVIOUSLY_DELIVERED = {
+ callback = function(task)
+ if not task:has_recipients(2) then return false end
+ local to = task:get_recipients(2)
+ local rcvds = task:get_header_full('Received')
+ if not rcvds then return false end
+ for _, rcvd in ipairs(rcvds) do
+ local _,_,addr = rcvd['decoded']:lower():find("%sfor%s<(.-)>")
+ if addr then
+ for _, toa in ipairs(to) do
+ if toa and toa.addr:lower() == addr then
+ return true, addr
+ end
+ end
+ return false
+ end
+ end
+ end,
+ description = 'Message either to a list or was forwarded',
+ score = 0.0
+}
+