1--[[
2Copyright (c) 2011-2017, Vsevolod Stakhov <vsevolod@highsecure.ru>
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15]]--
16
17-- Misc rules
18
19local E = {}
20local fun = require "fun"
21local util = require "rspamd_util"
22local rspamd_parsers = require "rspamd_parsers"
23local rspamd_regexp = require "rspamd_regexp"
24local rspamd_lua_utils = require "lua_util"
25local bit = require "bit"
26local rspamd_url = require "rspamd_url"
27local url_flags_tab = rspamd_url.flags
28
29-- Different text parts
30rspamd_config.R_PARTS_DIFFER = {
31  callback = function(task)
32    local distance = task:get_mempool():get_variable('parts_distance', 'double')
33
34    if distance then
35      local nd = tonumber(distance)
36      -- ND is relation of different words to total words
37      if nd >= 0.5 then
38        local tw = task:get_mempool():get_variable('total_words', 'int')
39
40        if tw then
41          local score
42          if tw > 30 then
43            -- We are confident about difference
44            score = (nd - 0.5) * 2.0
45          else
46            -- We are not so confident about difference
47            score = (nd - 0.5)
48          end
49          task:insert_result('R_PARTS_DIFFER', score,
50            string.format('%.1f%%', tostring(100.0 * nd)))
51        end
52      end
53    end
54    return false
55  end,
56  score = 1.0,
57  description = 'Text and HTML parts differ',
58  group = 'body'
59}
60
61-- Date issues
62local date_id = rspamd_config:register_symbol({
63  name = 'DATE_CB',
64  type = 'callback,mime',
65  callback = function(task)
66    local date_time = task:get_header('Date')
67    if date_time == nil or date_time == '' then
68      task:insert_result('MISSING_DATE', 1.0)
69      return
70    end
71
72    local dm, err = rspamd_parsers.parse_smtp_date(date_time)
73    if err then
74      task:insert_result('INVALID_DATE', 1.0)
75      return
76    end
77
78    local dt = task:get_date({format = 'connect', gmt = true})
79    local date_diff = dt - dm
80
81    if date_diff > 86400 then
82      -- Older than a day
83      task:insert_result('DATE_IN_PAST', 1.0, tostring(math.floor(date_diff/3600)))
84    elseif -date_diff > 7200 then
85      -- More than 2 hours in the future
86      task:insert_result('DATE_IN_FUTURE', 1.0, tostring(math.floor(-date_diff/3600)))
87    end
88  end
89})
90
91rspamd_config:register_symbol({
92  name = 'MISSING_DATE',
93  score = 1.0,
94  description = 'Message date is missing',
95  group = 'headers',
96  type = 'virtual',
97  parent = date_id,
98})
99
100rspamd_config:register_symbol({
101  name = 'INVALID_DATE',
102  score = 1.5,
103  description = 'Malformed date header',
104  group = 'headers',
105  type = 'virtual',
106  parent = date_id,
107})
108
109rspamd_config:register_symbol({
110  name = 'DATE_IN_FUTURE',
111  score = 4.0,
112  description = 'Message date is in future',
113  group = 'headers',
114  type = 'virtual',
115  parent = date_id,
116})
117
118rspamd_config:register_symbol({
119  name = 'DATE_IN_PAST',
120  score = 1.0,
121  description = 'Message date is in past',
122  group = 'headers',
123  type = 'virtual',
124  parent = date_id,
125})
126
127local obscured_id = rspamd_config:register_symbol{
128  callback = function(task)
129    local susp_urls = task:get_urls_filtered({ 'obscured', 'zw_spaces'})
130
131    if susp_urls and susp_urls[1] then
132      local obs_flag = url_flags_tab.obscured
133      local zw_flag = url_flags_tab.zw_spaces
134
135      for _,u in ipairs(susp_urls) do
136        local fl = u:get_flags_num()
137        if bit.band(fl, obs_flag) ~= 0 then
138          task:insert_result('R_SUSPICIOUS_URL', 1.0, u:get_host())
139        end
140        if bit.band(fl, zw_flag) ~= 0 then
141          task:insert_result('ZERO_WIDTH_SPACE_URL', 1.0, u:get_host())
142        end
143      end
144    end
145
146    return false
147  end,
148  name = 'R_SUSPICIOUS_URL',
149  score = 5.0,
150  one_shot = true,
151  description = 'Obfuscated or suspicious URL has been found in a message',
152  group = 'url'
153}
154
155rspamd_config:register_symbol{
156  type = 'virtual',
157  name = 'ZERO_WIDTH_SPACE_URL',
158  score = 7.0,
159  one_shot = true,
160  description = 'Zero width space in url',
161  group = 'url',
162  parent = obscured_id,
163}
164
165
166rspamd_config.ENVFROM_PRVS = {
167  callback = function (task)
168    --[[
169        Detect PRVS/BATV addresses to avoid FORGED_SENDER
170        https://en.wikipedia.org/wiki/Bounce_Address_Tag_Validation
171
172        Signature syntax:
173
174        prvs=TAG=USER@example.com       BATV draft (https://tools.ietf.org/html/draft-levine-smtp-batv-01)
175        prvs=USER=TAG@example.com
176        btv1==TAG==USER@example.com     Barracuda appliance
177        msprvs1=TAG=USER@example.com    Sparkpost email delivery service
178        ]]--
179    if not (task:has_from(1) and task:has_from(2)) then
180      return false
181    end
182    local envfrom = task:get_from(1)
183    local re_text = '^(?:(prvs|msprvs1)=([^=]+)=|btv1==[^=]+==)(.+@(.+))$'
184    local re = rspamd_regexp.create_cached(re_text)
185    local c = re:search(envfrom[1].addr:lower(), false, true)
186    if not c then return false end
187    local ef = c[1][4]
188    -- See if it matches the From header
189    local from = task:get_from(2)
190    if ef == from[1].addr:lower() then
191      return true
192    end
193    -- Check for prvs=USER=TAG@example.com
194    local t = c[1][2]
195    if t == 'prvs' then
196      local efr = c[1][3] .. '@' .. c[1][5]
197      if efr == from[1].addr:lower() then
198        return true
199      end
200    end
201    return false
202  end,
203  score = 0.0,
204  description = "Envelope From is a PRVS address that matches the From address",
205  group = 'headers',
206  type = 'mime',
207}
208
209rspamd_config.ENVFROM_VERP = {
210  callback = function (task)
211    if not (task:has_from(1) and task:has_recipients(1)) then
212      return false
213    end
214    local envfrom = task:get_from(1)
215    local envrcpts = task:get_recipients(1)
216    -- VERP only works for single recipient messages
217    if #envrcpts > 1 then return false end
218    -- Get recipient and compute VERP address
219    local rcpt = envrcpts[1].addr:lower()
220    local verp = rcpt:gsub('@','=')
221    -- Get the user portion of the envfrom
222    local ef_user = envfrom[1].user:lower()
223    -- See if the VERP representation of the recipient appears in it
224    if ef_user:find(verp, 1, true)
225      and not ef_user:find('+caf_=' .. verp, 1, true) -- Google Forwarding
226      and not ef_user:find('^srs[01]=')               -- SRS
227    then
228      return true
229    end
230    return false
231  end,
232  score = 0.0,
233  description = "Envelope From is a VERP address",
234  group = "headers",
235  type = 'mime',
236}
237
238local check_rcvd = rspamd_config:register_symbol{
239  name = 'CHECK_RCVD',
240  group = 'headers',
241  callback = function (task)
242    local rcvds = task:get_received_headers()
243    if not rcvds or #rcvds == 0 then return false end
244
245    local all_tls = fun.all(function(rc)
246      return rc.flags and rc.flags['ssl']
247    end, fun.filter(function(rc)
248      return rc.by_hostname and rc.by_hostname ~= 'localhost'
249    end, rcvds))
250
251    -- See if only the last hop was encrypted
252    if all_tls then
253      task:insert_result('RCVD_TLS_ALL', 1.0)
254    else
255      local rcvd = rcvds[1]
256      if rcvd.by_hostname and rcvd.by_hostname == 'localhost' then
257        -- Ignore artificial header from Rmilter
258        rcvd = rcvds[2] or {}
259      end
260      if rcvd.flags and rcvd.flags['ssl'] then
261        task:insert_result('RCVD_TLS_LAST', 1.0)
262      else
263        task:insert_result('RCVD_NO_TLS_LAST', 1.0)
264      end
265    end
266
267    local auth = fun.any(function(rc)
268      return rc.flags and rc.flags['authenticated']
269    end, rcvds)
270
271    if auth then
272      task:insert_result('RCVD_VIA_SMTP_AUTH', 1.0)
273    end
274  end,
275  type = 'callback,mime',
276}
277
278rspamd_config:register_symbol{
279  type = 'virtual',
280  parent = check_rcvd,
281  name = 'RCVD_TLS_ALL',
282  description = 'All hops used encrypted transports',
283  score = 0.0,
284  group = 'headers'
285}
286
287rspamd_config:register_symbol{
288  type = 'virtual',
289  parent = check_rcvd,
290  name = 'RCVD_TLS_LAST',
291  description = 'Last hop used encrypted transports',
292  score = 0.0,
293  group = 'headers'
294}
295
296rspamd_config:register_symbol{
297  type = 'virtual',
298  parent = check_rcvd,
299  name = 'RCVD_NO_TLS_LAST',
300  description = 'Last hop did not use encrypted transports',
301  score = 0.1,
302  group = 'headers'
303}
304
305rspamd_config:register_symbol{
306  type = 'virtual',
307  parent = check_rcvd,
308  name = 'RCVD_VIA_SMTP_AUTH',
309  -- NB This does not mean sender was authenticated; see task:get_user()
310  description = 'Authenticated hand-off was seen in Received headers',
311  score = 0.0,
312  group = 'headers'
313}
314
315rspamd_config.RCVD_HELO_USER = {
316  callback = function (task)
317    -- Check HELO argument from MTA
318    local helo = task:get_helo()
319    if (helo and helo:lower():find('^user$')) then
320      return true
321    end
322    -- Check Received headers
323    local rcvds = task:get_header_full('Received')
324    if not rcvds then return false end
325    for _, rcvd in ipairs(rcvds) do
326      local r = rcvd['decoded']:lower()
327      if (r:find("^%s*from%suser%s")) then return true end
328      if (r:find("helo[%s=]user[%s%)]")) then return true end
329    end
330  end,
331  description = 'HELO User spam pattern',
332  group = 'headers',
333  type = 'mime',
334  score = 3.0
335}
336
337rspamd_config.URI_COUNT_ODD = {
338  callback = function (task)
339    local ct = task:get_header('Content-Type')
340    if (ct and ct:lower():find('^multipart/alternative')) then
341      local urls = task:get_urls() or {}
342      local nurls = fun.filter(function(url)
343        return not url:is_html_displayed()
344      end, urls):foldl(function(acc, val) return acc + val:get_count() end, 0)
345
346      if nurls % 2 == 1 then
347        return true, 1.0, tostring(nurls)
348      end
349    end
350  end,
351  description = 'Odd number of URIs in multipart/alternative message',
352  score = 1.0,
353  group = 'url',
354}
355
356rspamd_config.HAS_ATTACHMENT = {
357  callback = function (task)
358    local parts = task:get_parts()
359    if parts and #parts > 1 then
360      for _, p in ipairs(parts) do
361        local cd = p:get_header('Content-Disposition')
362        if (cd and cd:lower():match('^attachment')) then
363          return true
364        end
365      end
366    end
367  end,
368  description = 'Message contains attachments',
369  group = 'body',
370}
371
372-- Requires freemail maps loaded in multimap
373local function freemail_reply_neq_from(task)
374  if not task:has_symbol('FREEMAIL_REPLYTO') or not task:has_symbol('FREEMAIL_FROM') then
375    return false
376  end
377  local frt = task:get_symbol('FREEMAIL_REPLYTO')
378  local ff = task:get_symbol('FREEMAIL_FROM')
379  local frt_opts = frt[1]['options']
380  local ff_opts = ff[1]['options']
381  return ( frt_opts and ff_opts and frt_opts[1] ~= ff_opts[1] )
382end
383
384rspamd_config:register_symbol({
385  name = 'FREEMAIL_REPLYTO_NEQ_FROM_DOM',
386  callback = freemail_reply_neq_from,
387  description = 'Freemail From and Reply-To, but to different Freemail services',
388  score = 3.0,
389  group = 'headers',
390})
391rspamd_config:register_dependency('FREEMAIL_REPLYTO_NEQ_FROM_DOM', 'FREEMAIL_REPLYTO')
392rspamd_config:register_dependency('FREEMAIL_REPLYTO_NEQ_FROM_DOM', 'FREEMAIL_FROM')
393
394rspamd_config.OMOGRAPH_URL = {
395  callback = function(task)
396    local urls = task:get_urls()
397
398    if urls then
399      local bad_omographs = 0
400      local single_bad_omograps = 0
401      local bad_urls = {}
402      local seen = {}
403
404      fun.each(function(u)
405        if u:is_phished() then
406
407          local h1 = u:get_host()
408          local h2 = u:get_phished()
409          if h2 then -- Due to changes of the phished flag in 2.8
410            h2 = h2:get_host()
411          end
412          if h1 and h2 then
413            local selt = string.format('%s->%s', h1, h2)
414            if not seen[selt] and util.is_utf_spoofed(h1, h2) then
415              bad_urls[#bad_urls + 1] = selt
416              bad_omographs = bad_omographs + 1
417            end
418            seen[selt] = true
419          end
420        end
421        if not u:is_html_displayed() then
422          local h = u:get_tld()
423
424          if h then
425            if not seen[h] and util.is_utf_spoofed(h) then
426              bad_urls[#bad_urls + 1] = h
427              single_bad_omograps = single_bad_omograps + 1
428            end
429            seen[h] = true
430          end
431        end
432      end, urls)
433
434      if bad_omographs > 0 then
435        return true, 1.0, bad_urls
436      elseif single_bad_omograps > 0 then
437        return true, 0.5, bad_urls
438      end
439    end
440
441    return false
442  end,
443  score = 5.0,
444  group = 'url',
445  description = 'Url contains both latin and non-latin characters'
446}
447
448rspamd_config.URL_IN_SUBJECT = {
449  callback = function(task)
450    local urls = task:get_urls()
451
452    if urls then
453      for _,u in ipairs(urls) do
454        local flags = u:get_flags()
455        if flags.subject then
456          if flags.schemaless then
457            return true,0.1,u:get_host()
458          end
459          local subject = task:get_subject()
460
461          if subject then
462            if tostring(u) == subject then
463              return true,1.0,u:get_host()
464            end
465          end
466          return true,0.25,u:get_host()
467        end
468      end
469    end
470
471    return false
472  end,
473  score = 4.0,
474  group = 'subject',
475  type = 'mime',
476  description = 'URL found in Subject'
477
478}
479
480local aliases_id = rspamd_config:register_symbol{
481  type = 'prefilter',
482  name = 'EMAIL_PLUS_ALIASES',
483  callback = function(task)
484    local function check_from(type)
485      if task:has_from(type) then
486        local addr = task:get_from(type)[1]
487        local na,tags = rspamd_lua_utils.remove_email_aliases(addr)
488        if na then
489          task:set_from(type, addr, 'alias')
490          task:insert_result('TAGGED_FROM', 1.0, fun.totable(
491            fun.filter(function(t) return t and #t > 0 end, tags)))
492        end
493      end
494    end
495
496    check_from('smtp')
497    check_from('mime')
498
499    local function check_rcpt(type)
500      if task:has_recipients(type) then
501        local modified = false
502        local all_tags = {}
503        local addrs = task:get_recipients(type)
504
505        for _, addr in ipairs(addrs) do
506          local na,tags = rspamd_lua_utils.remove_email_aliases(addr)
507          if na then
508            modified = true
509            fun.each(function(t) table.insert(all_tags, t) end,
510              fun.filter(function(t) return t and #t > 0 end, tags))
511          end
512        end
513
514        if modified then
515          task:set_recipients(type, addrs, 'alias')
516          task:insert_result('TAGGED_RCPT', 1.0, all_tags)
517        end
518      end
519    end
520
521    check_rcpt('smtp')
522    check_rcpt('mime')
523  end,
524  priority = 150,
525  description = 'Removes plus aliases from the email',
526  group = 'headers',
527}
528
529rspamd_config:register_symbol{
530  type = 'virtual',
531  parent = aliases_id,
532  name = 'TAGGED_RCPT',
533  description = 'SMTP recipients have plus tags',
534  group = 'headers',
535  score = 0.0,
536}
537rspamd_config:register_symbol{
538  type = 'virtual',
539  parent = aliases_id,
540  name = 'TAGGED_FROM',
541  description = 'SMTP from has plus tags',
542  group = 'headers',
543  score = 0.0,
544}
545
546local check_from_display_name = rspamd_config:register_symbol{
547  type = 'callback,mime',
548  name = 'FROM_DISPLAY_CALLBACK',
549  callback = function (task)
550    local from = task:get_from(2)
551    if not (from and from[1] and from[1].name) then return false end
552    -- See if we can parse an email address from the name
553    local parsed = rspamd_parsers.parse_mail_address(from[1].name, task:get_mempool())
554    if not parsed then return false end
555    if not (parsed[1] and parsed[1]['addr']) then return false end
556    -- Make sure we did not mistake e.g. <something>@<name> for an email address
557    if not parsed[1]['domain'] or not parsed[1]['domain']:find('%.') then return false end
558    -- See if the parsed domains differ
559    if not util.strequal_caseless(from[1]['domain'], parsed[1]['domain']) then
560      -- See if the destination domain is the same as the spoof
561      local mto = task:get_recipients(2)
562      local sto = task:get_recipients(1)
563      if mto then
564        for _, to in ipairs(mto) do
565          if to['domain'] ~= '' and util.strequal_caseless(to['domain'], parsed[1]['domain']) then
566            task:insert_result('SPOOF_DISPLAY_NAME', 1.0, from[1]['domain'], parsed[1]['domain'])
567            return false
568          end
569        end
570      end
571      if sto then
572        for _, to in ipairs(sto) do
573          if to['domain'] ~= '' and util.strequal_caseless(to['domain'], parsed[1]['domain']) then
574            task:insert_result('SPOOF_DISPLAY_NAME', 1.0, from[1]['domain'], parsed[1]['domain'])
575            return false
576          end
577        end
578      end
579      task:insert_result('FROM_NEQ_DISPLAY_NAME', 1.0, from[1]['domain'], parsed[1]['domain'])
580    end
581    return false
582  end,
583  group = 'headers',
584}
585
586rspamd_config:register_symbol{
587  type = 'virtual',
588  parent = check_from_display_name,
589  name = 'SPOOF_DISPLAY_NAME',
590  description = 'Display name is being used to spoof and trick the recipient',
591  group = 'headers',
592  score = 8.0,
593}
594
595rspamd_config:register_symbol{
596  type = 'virtual',
597  parent = check_from_display_name,
598  name = 'FROM_NEQ_DISPLAY_NAME',
599  group = 'headers',
600  description = 'Display name contains an email address different to the From address',
601  score = 4.0,
602}
603
604rspamd_config.SPOOF_REPLYTO = {
605  callback = function (task)
606    -- First check for a Reply-To header
607    local rt = task:get_header_full('Reply-To')
608    if not rt or not rt[1] then return false end
609    -- Get From and To headers
610    rt = rt[1]['value']
611    local from = task:get_from(2)
612    local to = task:get_recipients(2)
613    if not (from and from[1] and from[1].addr) then return false end
614    if (to and to[1] and to[1].addr) then
615      -- Handle common case for Web Contact forms of From = To
616      if util.strequal_caseless(from[1].addr, to[1].addr) then
617        return false
618      end
619    end
620    -- SMTP recipients must contain From domain
621    to = task:get_recipients(1)
622    if not to then return false end
623    -- Try mitigate some possible FPs on mailing list posts
624    if #to == 1 and util.strequal_caseless(to[1].addr, from[1].addr) then return false end
625    local found_fromdom = false
626    for _, t in ipairs(to) do
627      if util.strequal_caseless(t.domain, from[1].domain) then
628        found_fromdom = true
629        break
630      end
631    end
632    if not found_fromdom then return false end
633    -- Parse Reply-To header
634    local parsed = ((rspamd_parsers.parse_mail_address(rt, task:get_mempool()) or E)[1] or E).domain
635    if not parsed then return false end
636    -- Reply-To domain must be different to From domain
637    if not util.strequal_caseless(parsed, from[1].domain) then
638      return true, from[1].domain, parsed
639    end
640    return false
641  end,
642  group = 'headers',
643  type = 'mime',
644  description = 'Reply-To is being used to spoof and trick the recipient to send an off-domain reply',
645  score = 6.0
646}
647
648rspamd_config.INFO_TO_INFO_LU = {
649  callback = function(task)
650    if not task:has_header('List-Unsubscribe') then
651      return false
652    end
653    local from = task:get_from('mime')
654    if not (from and from[1] and util.strequal_caseless(from[1].user, 'info')) then
655      return false
656    end
657    local to = task:get_recipients('smtp')
658    if not to then return false end
659    local found = false
660    for _,r in ipairs(to) do
661      if util.strequal_caseless(r['user'], 'info') then
662        found = true
663      end
664    end
665    if found then return true end
666    return false
667  end,
668  description = 'info@ From/To address with List-Unsubscribe headers',
669  group = 'headers',
670  score = 2.0,
671  type = 'mime',
672}
673
674-- Detects bad content-transfer-encoding for text parts
675
676rspamd_config.R_BAD_CTE_7BIT = {
677  callback = function(task)
678    local tp = task:get_text_parts() or {}
679
680    for _,p in ipairs(tp) do
681      local cte = p:get_mimepart():get_cte() or ''
682      if cte ~= '8bit' and p:has_8bit_raw() then
683        local _,_,attrs = p:get_mimepart():get_type_full()
684        local mul = 1.0
685        local params = {cte}
686        if attrs then
687          if attrs.charset and attrs.charset:lower() == "utf-8" then
688            -- Penalise rule as people don't know that utf8 is surprisingly
689            -- eight bit encoding
690            mul = 0.3
691            table.insert(params, "utf8")
692          end
693        end
694
695        return true,mul,params
696      end
697    end
698
699    return false
700  end,
701  score = 3.5,
702  description = 'Detects bad content-transfer-encoding for text parts',
703  group = 'headers',
704  type = 'mime',
705}
706
707
708local check_encrypted_name = rspamd_config:register_symbol{
709  name = 'BOGUS_ENCRYPTED_AND_TEXT',
710  callback = function(task)
711    local parts = task:get_parts() or {}
712    local seen_encrypted, seen_text
713    local opts = {}
714
715    local function check_part(part)
716      if part:is_multipart() then
717        local children = part:get_children() or {}
718        local text_kids = {}
719
720        for _,cld in ipairs(children) do
721          if cld:is_multipart() then
722            check_part(cld)
723          elseif cld:is_text() then
724            seen_text = true
725            text_kids[#text_kids + 1] = cld
726          else
727            local type,subtype,_ = cld:get_type_full()
728
729            if type:lower() == 'application' then
730              if string.find(subtype:lower(), 'pkcs7%-mime') then
731                -- S/MIME encrypted part
732                seen_encrypted = true
733                table.insert(opts, 'smime part')
734                task:insert_result('ENCRYPTED_SMIME', 1.0)
735              elseif string.find(subtype:lower(), 'pkcs7%-signature') then
736                task:insert_result('SIGNED_SMIME', 1.0)
737              elseif string.find(subtype:lower(), 'pgp%-encrypted') then
738                -- PGP/GnuPG encrypted part
739                seen_encrypted = true
740                table.insert(opts, 'pgp part')
741                task:insert_result('ENCRYPTED_PGP', 1.0)
742              elseif string.find(subtype:lower(), 'pgp%-signature') then
743                task:insert_result('SIGNED_PGP', 1.0)
744              end
745            end
746          end
747          if seen_text and seen_encrypted then
748            -- Ensure that our seen text is not really part of pgp #3205
749            for _,tp in ipairs(text_kids) do
750              local t,_ = tp:get_type()
751              seen_text = false -- reset temporary
752              if t and t == 'text' then
753                seen_text = true
754                break
755              end
756            end
757          end
758        end
759      end
760    end
761
762    for _,part in ipairs(parts) do
763      check_part(part)
764    end
765
766    if seen_text and seen_encrypted then
767      return true, 1.0, opts
768    end
769
770    return false
771  end,
772  score = 10.0,
773  description = 'Bogus mix of encrypted and text/html payloads',
774  group = 'mime_types',
775}
776
777rspamd_config:register_symbol{
778  type = 'virtual',
779  parent = check_encrypted_name,
780  name = 'ENCRYPTED_PGP',
781  description = 'Message is encrypted with pgp',
782  group = 'mime_types',
783  score = -0.5,
784  one_shot = true
785}
786
787rspamd_config:register_symbol{
788  type = 'virtual',
789  parent = check_encrypted_name,
790  name = 'ENCRYPTED_SMIME',
791  description = 'Message is encrypted with smime',
792  group = 'mime_types',
793  score = -0.5,
794  one_shot = true
795}
796
797rspamd_config:register_symbol{
798  type = 'virtual',
799  parent = check_encrypted_name,
800  name = 'SIGNED_PGP',
801  description = 'Message is signed with pgp',
802  group = 'mime_types',
803  score = -2.0,
804  one_shot = true
805}
806
807rspamd_config:register_symbol{
808  type = 'virtual',
809  parent = check_encrypted_name,
810  name = 'SIGNED_SMIME',
811  description = 'Message is signed with smime',
812  group = 'mime_types',
813  score = -2.0,
814  one_shot = true
815}
816