Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

misc.lua 23KB

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