You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

misc.lua 21KB

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