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

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