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 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  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. local seen = {}
  340. fun.each(function(u)
  341. if u:is_phished() then
  342. local h1 = u:get_host()
  343. local h2 = u:get_phished():get_host()
  344. if h1 and h2 then
  345. local selt = string.format('%s->%s', h1, h2)
  346. if not seen[selt] and util.is_utf_spoofed(h1, h2) then
  347. bad_urls[#bad_urls + 1] = selt
  348. bad_omographs = bad_omographs + 1
  349. end
  350. seen[selt] = true
  351. end
  352. end
  353. if not u:is_html_displayed() then
  354. local h = u:get_tld()
  355. if h then
  356. if not seen[h] and util.is_utf_spoofed(h) then
  357. bad_urls[#bad_urls + 1] = h
  358. single_bad_omograps = single_bad_omograps + 1
  359. end
  360. seen[h] = true
  361. end
  362. end
  363. end, urls)
  364. if bad_omographs > 0 then
  365. return true, 1.0, bad_urls
  366. elseif single_bad_omograps > 0 then
  367. return true, 0.5, bad_urls
  368. end
  369. end
  370. return false
  371. end,
  372. score = 5.0,
  373. group = 'url',
  374. description = 'Url contains both latin and non-latin characters'
  375. }
  376. rspamd_config.URL_IN_SUBJECT = {
  377. callback = function(task)
  378. local urls = task:get_urls()
  379. if urls then
  380. for _,u in ipairs(urls) do
  381. local flags = u:get_flags()
  382. if flags.subject then
  383. if flags.schemaless then
  384. return true,0.1,u:get_host()
  385. end
  386. local subject = task:get_subject()
  387. if subject then
  388. if tostring(u) == subject then
  389. return true,1.0,u:get_host()
  390. end
  391. end
  392. return true,0.25,u:get_host()
  393. end
  394. end
  395. end
  396. return false
  397. end,
  398. score = 4.0,
  399. group = 'subject',
  400. type = 'mime',
  401. description = 'URL found in Subject'
  402. }
  403. local aliases_id = rspamd_config:register_symbol{
  404. type = 'prefilter',
  405. name = 'EMAIL_PLUS_ALIASES',
  406. callback = function(task)
  407. local function check_from(type)
  408. if task:has_from(type) then
  409. local addr = task:get_from(type)[1]
  410. local na,tags = rspamd_lua_utils.remove_email_aliases(addr)
  411. if na then
  412. task:set_from(type, addr)
  413. task:insert_result('TAGGED_FROM', 1.0, fun.totable(
  414. fun.filter(function(t) return t and #t > 0 end, tags)))
  415. end
  416. end
  417. end
  418. check_from('smtp')
  419. check_from('mime')
  420. local function check_rcpt(type)
  421. if task:has_recipients(type) then
  422. local modified = false
  423. local all_tags = {}
  424. local addrs = task:get_recipients(type)
  425. for _, addr in ipairs(addrs) do
  426. local na,tags = rspamd_lua_utils.remove_email_aliases(addr)
  427. if na then
  428. modified = true
  429. fun.each(function(t) table.insert(all_tags, t) end,
  430. fun.filter(function(t) return t and #t > 0 end, tags))
  431. end
  432. end
  433. if modified then
  434. task:set_recipients(type, addrs)
  435. task:insert_result('TAGGED_RCPT', 1.0, all_tags)
  436. end
  437. end
  438. end
  439. check_rcpt('smtp')
  440. check_rcpt('mime')
  441. end,
  442. priority = 150,
  443. description = 'Removes plus aliases from the email',
  444. group = 'headers',
  445. }
  446. rspamd_config:register_symbol{
  447. type = 'virtual',
  448. parent = aliases_id,
  449. name = 'TAGGED_RCPT',
  450. description = 'SMTP recipients have plus tags',
  451. group = 'headers',
  452. score = 0,
  453. }
  454. rspamd_config:register_symbol{
  455. type = 'virtual',
  456. parent = aliases_id,
  457. name = 'TAGGED_FROM',
  458. description = 'SMTP from has plus tags',
  459. group = 'headers',
  460. score = 0,
  461. }
  462. local check_from_display_name = rspamd_config:register_symbol{
  463. type = 'callback,mime',
  464. name = 'FROM_DISPLAY_CALLBACK',
  465. callback = function (task)
  466. local from = task:get_from(2)
  467. if not (from and from[1] and from[1].name) then return false end
  468. -- See if we can parse an email address from the name
  469. local parsed = util.parse_mail_address(from[1].name, task:get_mempool())
  470. if not parsed then return false end
  471. if not (parsed[1] and parsed[1]['addr']) then return false end
  472. -- Make sure we did not mistake e.g. <something>@<name> for an email address
  473. if not parsed[1]['domain'] or not parsed[1]['domain']:find('%.') then return false end
  474. -- See if the parsed domains differ
  475. if not util.strequal_caseless(from[1]['domain'], parsed[1]['domain']) then
  476. -- See if the destination domain is the same as the spoof
  477. local mto = task:get_recipients(2)
  478. local sto = task:get_recipients(1)
  479. if mto then
  480. for _, to in ipairs(mto) do
  481. if to['domain'] ~= '' and util.strequal_caseless(to['domain'], parsed[1]['domain']) then
  482. task:insert_result('SPOOF_DISPLAY_NAME', 1.0, from[1]['domain'], parsed[1]['domain'])
  483. return false
  484. end
  485. end
  486. end
  487. if sto then
  488. for _, to in ipairs(sto) do
  489. if to['domain'] ~= '' and util.strequal_caseless(to['domain'], parsed[1]['domain']) then
  490. task:insert_result('SPOOF_DISPLAY_NAME', 1.0, from[1]['domain'], parsed[1]['domain'])
  491. return false
  492. end
  493. end
  494. end
  495. task:insert_result('FROM_NEQ_DISPLAY_NAME', 1.0, from[1]['domain'], parsed[1]['domain'])
  496. end
  497. return false
  498. end,
  499. group = 'headers',
  500. }
  501. rspamd_config:register_symbol{
  502. type = 'virtual',
  503. parent = check_from_display_name,
  504. name = 'SPOOF_DISPLAY_NAME',
  505. description = 'Display name is being used to spoof and trick the recipient',
  506. group = 'headers',
  507. score = 8,
  508. }
  509. rspamd_config:register_symbol{
  510. type = 'virtual',
  511. parent = check_from_display_name,
  512. name = 'FROM_NEQ_DISPLAY_NAME',
  513. group = 'headers',
  514. description = 'Display name contains an email address different to the From address',
  515. score = 4,
  516. }
  517. rspamd_config.SPOOF_REPLYTO = {
  518. callback = function (task)
  519. -- First check for a Reply-To header
  520. local rt = task:get_header_full('Reply-To')
  521. if not rt or not rt[1] then return false end
  522. -- Get From and To headers
  523. rt = rt[1]['value']
  524. local from = task:get_from(2)
  525. local to = task:get_recipients(2)
  526. if not (from and from[1] and from[1].addr) then return false end
  527. if (to and to[1] and to[1].addr) then
  528. -- Handle common case for Web Contact forms of From = To
  529. if util.strequal_caseless(from[1].addr, to[1].addr) then
  530. return false
  531. end
  532. end
  533. -- SMTP recipients must contain From domain
  534. to = task:get_recipients(1)
  535. if not to then return false end
  536. -- Try mitigate some possible FPs on mailing list posts
  537. if #to == 1 and util.strequal_caseless(to[1].addr, from[1].addr) then return false end
  538. local found_fromdom = false
  539. for _, t in ipairs(to) do
  540. if util.strequal_caseless(t.domain, from[1].domain) then
  541. found_fromdom = true
  542. break
  543. end
  544. end
  545. if not found_fromdom then return false end
  546. -- Parse Reply-To header
  547. local parsed = ((util.parse_mail_address(rt, task:get_mempool()) or E)[1] or E).domain
  548. if not parsed then return false end
  549. -- Reply-To domain must be different to From domain
  550. if not util.strequal_caseless(parsed, from[1].domain) then
  551. return true, from[1].domain, parsed
  552. end
  553. return false
  554. end,
  555. group = 'headers',
  556. type = 'mime',
  557. description = 'Reply-To is being used to spoof and trick the recipient to send an off-domain reply',
  558. score = 6.0
  559. }
  560. rspamd_config.INFO_TO_INFO_LU = {
  561. callback = function(task)
  562. local lu = task:get_header('List-Unsubscribe')
  563. if not lu then return false end
  564. local from = task:get_from('mime')
  565. if not (from and from[1] and util.strequal_caseless(from[1].user, 'info')) then
  566. return false
  567. end
  568. local to = task:get_recipients('smtp')
  569. if not to then return false end
  570. local found = false
  571. for _,r in ipairs(to) do
  572. if util.strequal_caseless(r['user'], 'info') then
  573. found = true
  574. end
  575. end
  576. if found then return true end
  577. return false
  578. end,
  579. description = 'info@ From/To address with List-Unsubscribe headers',
  580. group = 'headers',
  581. score = 2.0,
  582. type = 'mime',
  583. }
  584. -- Detects bad content-transfer-encoding for text parts
  585. rspamd_config.R_BAD_CTE_7BIT = {
  586. callback = function(task)
  587. local tp = task:get_text_parts() or {}
  588. for _,p in ipairs(tp) do
  589. local cte = p:get_mimepart():get_cte() or ''
  590. if cte ~= '8bit' and p:has_8bit_raw() then
  591. local _,_,attrs = p:get_mimepart():get_type_full()
  592. local mul = 1.0
  593. local params = {cte}
  594. if attrs then
  595. if attrs.charset and attrs.charset:lower() == "utf-8" then
  596. -- Penalise rule as people don't know that utf8 is surprisingly
  597. -- eight bit encoding
  598. mul = 0.3
  599. table.insert(params, "utf8")
  600. end
  601. end
  602. return true,mul,params
  603. end
  604. end
  605. return false
  606. end,
  607. score = 3.5,
  608. description = 'Detects bad content-transfer-encoding for text parts',
  609. group = 'headers',
  610. type = 'mime',
  611. }
  612. local check_encrypted_name = rspamd_config:register_symbol{
  613. name = 'BOGUS_ENCRYPTED_AND_TEXT',
  614. callback = function(task)
  615. local parts = task:get_parts() or {}
  616. local seen_encrypted, seen_text
  617. local opts = {}
  618. local function check_part(part)
  619. if part:is_multipart() then
  620. local children = part:get_children() or {}
  621. for _,cld in ipairs(children) do
  622. if cld:is_multipart() then
  623. check_part(cld)
  624. elseif cld:is_text() then
  625. seen_text = true
  626. else
  627. local type,subtype,_ = cld:get_type_full()
  628. if type:lower() == 'application' then
  629. if string.find(subtype:lower(), 'pkcs7%-mime') then
  630. -- S/MIME encrypted part
  631. seen_encrypted = true
  632. table.insert(opts, 'smime part')
  633. task:insert_result('ENCRYPTED_SMIME', 1.0)
  634. elseif string.find(subtype:lower(), 'pkcs7%-signature') then
  635. task:insert_result('SIGNED_SMIME', 1.0)
  636. elseif string.find(subtype:lower(), 'pgp%-encrypted') then
  637. -- PGP/GnuPG encrypted part
  638. seen_encrypted = true
  639. table.insert(opts, 'pgp part')
  640. task:insert_result('ENCRYPTED_PGP', 1.0)
  641. elseif string.find(subtype:lower(), 'pgp%-signature') then
  642. task:insert_result('SIGNED_PGP', 1.0)
  643. end
  644. end
  645. end
  646. end
  647. end
  648. end
  649. for _,part in ipairs(parts) do
  650. check_part(part)
  651. end
  652. if seen_text and seen_encrypted then
  653. return true, 1.0, opts
  654. end
  655. return false
  656. end,
  657. score = 10.0,
  658. description = 'Bogus mix of encrypted and text/html payloads',
  659. group = 'mime_types',
  660. }
  661. rspamd_config:register_symbol{
  662. type = 'virtual',
  663. parent = check_encrypted_name,
  664. name = 'ENCRYPTED_PGP',
  665. description = 'Message is encrypted with pgp',
  666. group = 'mime_types',
  667. score = -0.5,
  668. one_shot = true
  669. }
  670. rspamd_config:register_symbol{
  671. type = 'virtual',
  672. parent = check_encrypted_name,
  673. name = 'ENCRYPTED_SMIME',
  674. description = 'Message is encrypted with smime',
  675. group = 'mime_types',
  676. score = -0.5,
  677. one_shot = true
  678. }
  679. rspamd_config:register_symbol{
  680. type = 'virtual',
  681. parent = check_encrypted_name,
  682. name = 'SIGNED_PGP',
  683. description = 'Message is signed with pgp',
  684. group = 'mime_types',
  685. score = -2.0,
  686. one_shot = true
  687. }
  688. rspamd_config:register_symbol{
  689. type = 'virtual',
  690. parent = check_encrypted_name,
  691. name = 'SIGNED_SMIME',
  692. description = 'Message is signed with smime',
  693. group = 'mime_types',
  694. score = -2.0,
  695. one_shot = true
  696. }