aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSteve Freegard <steve@stevefreegard.com>2016-11-21 12:55:14 +0000
committerSteve Freegard <steve@stevefreegard.com>2016-11-21 12:55:14 +0000
commit5c669479a0e0630f822929714332b615f11210a6 (patch)
tree80db15bb85dfc64df81c92b4369480eca9aafe2a
parent919cbd477d499804b17c87656a435db6067ca31e (diff)
downloadrspamd-5c669479a0e0630f822929714332b615f11210a6.tar.gz
rspamd-5c669479a0e0630f822929714332b615f11210a6.zip
Rules updates
-rw-r--r--rules/misc.lua582
-rw-r--r--rules/regexp/headers.lua91
-rw-r--r--rules/regexp/misc.lua39
-rw-r--r--rules/rspamd.lua1
4 files changed, 713 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
+}
+
diff --git a/rules/regexp/headers.lua b/rules/regexp/headers.lua
index ef0adc6b1..e5bce8cea 100644
--- a/rules/regexp/headers.lua
+++ b/rules/regexp/headers.lua
@@ -255,6 +255,22 @@ reconf['CC_EXCESS_QP'] = {
group = 'excessqp'
}
+local subj_encoded_b64 = 'Subject=/\\=\\?\\S+\\?B\\?/iX'
+local subj_needs_mime = 'Subject=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/Hr'
+reconf['SUBJ_EXCESS_BASE64'] = {
+ re = string.format('%s & !%s', subj_encoded_b64, subj_needs_mime),
+ score = 1.5,
+ description = 'Subject is unnecessarily encoded in base64',
+ group = 'excessb64'
+}
+
+local subj_encoded_qp = 'Subject=/\\=\\?\\S+\\?Q\\?/iX'
+reconf['SUBJ_EXCESS_QP'] = {
+ re = string.format('%s & !%s', subj_encoded_qp, subj_needs_mime),
+ score = 1.2,
+ description = 'Subect is unnecessarily encoded in quoted-printable',
+ group = 'excessqp'
+}
-- Detect forged outlook headers
-- OE X-Mailer header
@@ -803,3 +819,78 @@ reconf['GOOGLE_FORWARDING_MID_BROKEN'] = {
description = "Message had invalid Message-ID pre-forwarding",
group = 'header'
}
+
+reconf['CTE_CASE'] = {
+ re = 'Content-Transfer-Encoding=/^[78]BsX',
+ description = '[78]Bit .vs. [78]bit',
+ score = 0.5,
+ group = header'
+}
+
+reconf['HAS_INTERSPIRE_SIG'] = {
+ re = string.format('((%s) & (%s) & (%s) & (%s)) | (%s)',
+ 'header_exists(X-Mailer-LID)',
+ 'header_exists(X-Mailer-RecptId)',
+ 'header_exists(X-Mailer-SID)',
+ 'header_exists(X-Mailer-Sent-By)',
+ 'List-Unsubscribe=/\\/unsubscribe\\.php\\?M=[^&]+&C=[^&]+&L=[^&]+&N=[^>]+>$/Xi'),
+ description = "Has Interspire fingerprint",
+ score = 3.0,
+ group = 'header'
+}
+
+reconf['CT_EXTRA_SEMI'] = {
+ re = 'Content-Type=/;$/X',
+ description = 'Content-Type ends with a semi-colon',
+ score = 1.0,
+ group = 'header'
+}
+
+reconf['SUBJECT_ENDS_EXCLAIM'] = {
+ re = 'Subject=/!\\s*$/H',
+ description = 'Subject ends with an exclaimation',
+ score = 1.0,
+ group = 'headers'
+}
+
+reconf['SUBJECT_HAS_EXCLAIM'] = {
+ re = string.format('%s & !%s', 'Subject=/!/H', 'Subject=/!\\s*$/H'),
+ description = 'Subject contains an exclaimation',
+ score = 0.0,
+ group = 'headers'
+}
+
+reconf['SUBJECT_ENDS_QUESTION'] = {
+ re = 'Subject=/\\?\\s*$/H',
+ description = 'Subject ends with a question',
+ score = 1.0,
+ group = 'headers'
+}
+
+reconf['SUBJECT_HAS_QUESTION'] = {
+ re = string.format('%s & !%s', 'Subject=/\\?/H', 'Subject=/\\?\\s*$/H'),
+ description = 'Subject contains a question',
+ score = 0.0,
+ group = 'headers'
+}
+
+reconf['SUBJECT_HAS_CURRENCY'] = {
+ re = 'Subject=/$€$¢¥₽/H',
+ description = 'Subject contains currency',
+ score = 1.0,
+ group = 'headers'
+}
+
+reconf['SUBJECT_ENDS_SPACES'] = {
+ re = 'Subject=/\\s+$/H',
+ description = 'Subject ends with space characters',
+ score = 0.5,
+ group = 'headers'
+}
+
+reconf['HAS_ORG_HEADER'] = {
+ re = string.format('%s || %s', 'header_exists(Organization)', 'header_exists(Organisation)'),
+ description = 'Has Organization header',
+ score = 0.0,
+ group = 'headers'
+}
diff --git a/rules/regexp/misc.lua b/rules/regexp/misc.lua
new file mode 100644
index 000000000..a819ec729
--- /dev/null
+++ b/rules/regexp/misc.lua
@@ -0,0 +1,39 @@
+--[[
+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.
+]]--
+
+
+local reconf = config['regexp']
+
+reconf['HTML_META_REFRESH_URL'] = {
+ -- Requires options { check_attachements = true; }
+ re = '/<meta\\s+http-equiv="refresh"\\s+content="\\d+;url=/{sa_raw_body}i',
+ description = "Has HTML Meta refresh URL",
+ score = 5.0
+}
+
+reconf['HAS_DATA_URI'] = {
+ -- Requires options { check_attachements = true; }
+ re = '/data:[^\\/]+\\/[^; ]+;base64,/{sa_raw_body}i',
+ description = "Has Data URI encoding"
+}
+
+reconf['DATA_URI_OBFU'] = {
+ -- Requires options { check_attachements = true; }
+ re = '/data:text\\/(?:plain|html);base64,/{sa_raw_body}i',
+ description = "Uses Data URI encoding to obfuscate plain or HTML in base64",
+ score = 2.0
+}
+
diff --git a/rules/rspamd.lua b/rules/rspamd.lua
index f5a5ed14e..b02e6b1af 100644
--- a/rules/rspamd.lua
+++ b/rules/rspamd.lua
@@ -25,6 +25,7 @@ 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/misc.lua')
dofile(local_rules .. '/regexp/upstream_spam_filters.lua')
dofile(local_rules .. '/regexp/compromised_hosts.lua')
dofile(local_rules .. '/html.lua')