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.

headers_checks.lua 27KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045
  1. --[[
  2. Copyright (c) 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. local util = require "rspamd_util"
  14. local ipairs = ipairs
  15. local pairs = pairs
  16. local table = table
  17. local tostring = tostring
  18. local tonumber = tonumber
  19. local fun = require "fun"
  20. local E = {}
  21. local rcvd_cb_id = rspamd_config:register_symbol{
  22. name = 'CHECK_RECEIVED',
  23. type = 'callback',
  24. callback = function(task)
  25. local cnts = {
  26. [1] = 'ONE',
  27. [2] = 'TWO',
  28. [3] = 'THREE',
  29. [5] = 'FIVE',
  30. [7] = 'SEVEN',
  31. [12] = 'TWELVE'
  32. }
  33. local def = 'ZERO'
  34. local received = task:get_received_headers()
  35. local nreceived = fun.reduce(function(acc, rcvd)
  36. return acc + 1
  37. end, 0, fun.filter(function(h)
  38. return not h['artificial']
  39. end, received))
  40. for k,v in pairs(cnts) do
  41. if nreceived >= tonumber(k) then
  42. def = v
  43. end
  44. end
  45. task:insert_result('RCVD_COUNT_' .. def, 1.0, tostring(nreceived))
  46. end
  47. }
  48. rspamd_config:register_symbol{
  49. name = 'RCVD_COUNT_ZERO',
  50. score = 0.0,
  51. parent = rcvd_cb_id,
  52. type = 'virtual',
  53. description = 'No received',
  54. group = 'headers',
  55. }
  56. rspamd_config:register_symbol{
  57. name = 'RCVD_COUNT_ONE',
  58. score = 0.0,
  59. parent = rcvd_cb_id,
  60. type = 'virtual',
  61. description = 'One received',
  62. group = 'headers',
  63. }
  64. rspamd_config:register_symbol{
  65. name = 'RCVD_COUNT_TWO',
  66. score = 0.0,
  67. parent = rcvd_cb_id,
  68. type = 'virtual',
  69. description = 'Two received',
  70. group = 'header',
  71. }
  72. rspamd_config:register_symbol{
  73. name = 'RCVD_COUNT_THREE',
  74. score = 0.0,
  75. parent = rcvd_cb_id,
  76. type = 'virtual',
  77. description = '3-5 received',
  78. group = 'headers',
  79. }
  80. rspamd_config:register_symbol{
  81. name = 'RCVD_COUNT_FIVE',
  82. score = 0.0,
  83. parent = rcvd_cb_id,
  84. type = 'virtual',
  85. description = '5-7 received',
  86. group = 'headers',
  87. }
  88. rspamd_config:register_symbol{
  89. name = 'RCVD_COUNT_SEVEN',
  90. score = 0.0,
  91. parent = rcvd_cb_id,
  92. type = 'virtual',
  93. description = '7-11 received',
  94. group = 'headers',
  95. }
  96. rspamd_config:register_symbol{
  97. name = 'RCVD_COUNT_TWELVE',
  98. score = 0.0,
  99. parent = rcvd_cb_id,
  100. type = 'virtual',
  101. description = '12+ received',
  102. group = 'headers',
  103. }
  104. local prio_cb_id = rspamd_config:register_symbol {
  105. name = 'HAS_X_PRIO',
  106. type = 'callback',
  107. callback = function (task)
  108. local cnts = {
  109. [1] = 'ONE',
  110. [2] = 'TWO',
  111. [3] = 'THREE',
  112. [5] = 'FIVE',
  113. }
  114. local def = 'ZERO'
  115. local xprio = task:get_header('X-Priority');
  116. if not xprio then return false end
  117. local _,_,x = xprio:find('^%s?(%d+)');
  118. if (x) then
  119. x = tonumber(x)
  120. for k,v in pairs(cnts) do
  121. if x >= tonumber(k) then
  122. def = v
  123. end
  124. end
  125. task:insert_result('HAS_X_PRIO_' .. def, 1.0, tostring(x))
  126. end
  127. end
  128. }
  129. rspamd_config:register_symbol{
  130. name = 'HAS_X_PRIO_ZERO',
  131. score = 0.0,
  132. parent = prio_cb_id,
  133. type = 'virtual',
  134. description = 'Priority 0',
  135. group = 'headers',
  136. }
  137. rspamd_config:register_symbol{
  138. name = 'HAS_X_PRIO_ONE',
  139. score = 0.0,
  140. parent = prio_cb_id,
  141. type = 'virtual',
  142. description = 'Priority 1',
  143. group = 'headers',
  144. }
  145. rspamd_config:register_symbol{
  146. name = 'HAS_X_PRIO_TWO',
  147. score = 0.0,
  148. parent = prio_cb_id,
  149. type = 'virtual',
  150. description = 'Priority 2',
  151. group = 'headers',
  152. }
  153. rspamd_config:register_symbol{
  154. name = 'HAS_X_PRIO_THREE',
  155. score = 0.0,
  156. parent = prio_cb_id,
  157. type = 'virtual',
  158. description = 'Priority 3-4',
  159. group = 'headers',
  160. }
  161. rspamd_config:register_symbol{
  162. name = 'HAS_X_PRIO_FIVE',
  163. score = 0.0,
  164. parent = prio_cb_id,
  165. type = 'virtual',
  166. description = 'Priority 5+',
  167. group = 'headers',
  168. }
  169. local function get_raw_header(task, name)
  170. return ((task:get_header_full(name) or {})[1] or {})['value']
  171. end
  172. local check_replyto_id = rspamd_config:register_callback_symbol('CHECK_REPLYTO', 1.0,
  173. function (task)
  174. local replyto = get_raw_header(task, 'Reply-To')
  175. if not replyto then return false end
  176. local rt = util.parse_mail_address(replyto, task:get_mempool())
  177. if not (rt and rt[1] and (string.len(rt[1].addr) > 0)) then
  178. task:insert_result('REPLYTO_UNPARSEABLE', 1.0)
  179. return false
  180. else
  181. local rta = rt[1].addr
  182. task:insert_result('HAS_REPLYTO', 1.0, rta)
  183. -- Check if Reply-To address starts with title seen in display name
  184. local sym = task:get_symbol('FROM_NAME_HAS_TITLE')
  185. local title = (((sym or E)[1] or E).options or E)[1]
  186. if title then
  187. rta = rta:lower()
  188. if rta:find('^' .. title) then
  189. task:insert_result('REPLYTO_EMAIL_HAS_TITLE', 1.0)
  190. end
  191. end
  192. end
  193. -- See if Reply-To matches From in some way
  194. local from = task:get_from(2)
  195. local from_h = get_raw_header(task, 'From')
  196. if not (from and from[1]) then return false end
  197. if (from_h and from_h == replyto) then
  198. -- From and Reply-To are identical
  199. task:insert_result('REPLYTO_EQ_FROM', 1.0)
  200. else
  201. if (from and from[1]) then
  202. -- See if From and Reply-To addresses match
  203. if (util.strequal_caseless(from[1].addr, rt[1].addr)) then
  204. task:insert_result('REPLYTO_ADDR_EQ_FROM', 1.0)
  205. elseif from[1].domain and rt[1].domain then
  206. if (util.strequal_caseless(from[1].domain, rt[1].domain)) then
  207. task:insert_result('REPLYTO_DOM_EQ_FROM_DOM', 1.0)
  208. else
  209. -- See if Reply-To matches the To address
  210. local to = task:get_recipients(2)
  211. if (to and to[1] and to[1].addr:lower() == rt[1].addr:lower()) then
  212. -- Ignore this for mailing-lists and automatic submissions
  213. if (not (task:get_header('List-Unsubscribe') or
  214. task:get_header('X-List') or
  215. task:get_header('Auto-Submitted')))
  216. then
  217. task:insert_result('REPLYTO_EQ_TO_ADDR', 1.0)
  218. end
  219. else
  220. task:insert_result('REPLYTO_DOM_NEQ_FROM_DOM', 1.0)
  221. end
  222. end
  223. end
  224. -- See if the Display Names match
  225. if (from[1].name and rt[1].name and
  226. util.strequal_caseless(from[1].name, rt[1].name)) then
  227. task:insert_result('REPLYTO_DN_EQ_FROM_DN', 1.0)
  228. end
  229. end
  230. end
  231. end
  232. )
  233. rspamd_config:register_symbol{
  234. name = 'REPLYTO_UNPARSEABLE',
  235. score = 1.0,
  236. parent = check_replyto_id,
  237. type = 'virtual',
  238. description = 'Reply-To header could not be parsed',
  239. group = 'headers',
  240. }
  241. rspamd_config:register_symbol{
  242. name = 'HAS_REPLYTO',
  243. score = 0.0,
  244. parent = check_replyto_id,
  245. type = 'virtual',
  246. description = 'Has Reply-To header',
  247. group = 'headers',
  248. }
  249. rspamd_config:register_symbol{
  250. name = 'REPLYTO_EQ_FROM',
  251. score = 0.0,
  252. parent = check_replyto_id,
  253. type = 'virtual',
  254. description = 'Reply-To header is identical to From header',
  255. group = 'headers',
  256. }
  257. rspamd_config:register_symbol{
  258. name = 'REPLYTO_ADDR_EQ_FROM',
  259. score = 0.0,
  260. parent = check_replyto_id,
  261. type = 'virtual',
  262. description = 'Reply-To header is identical to SMTP From',
  263. group = 'headers',
  264. }
  265. rspamd_config:register_symbol{
  266. name = 'REPLYTO_DOM_EQ_FROM_DOM',
  267. score = 0.0,
  268. parent = check_replyto_id,
  269. type = 'virtual',
  270. description = 'Reply-To domain matches the From domain',
  271. group = 'headers',
  272. }
  273. rspamd_config:register_symbol{
  274. name = 'REPLYTO_DOM_NEQ_FROM_DOM',
  275. score = 0.0,
  276. parent = check_replyto_id,
  277. type = 'virtual',
  278. description = 'Reply-To domain does not match the From domain',
  279. group = 'headers',
  280. }
  281. rspamd_config:register_symbol{
  282. name = 'REPLYTO_DN_EQ_FROM_DN',
  283. score = 0.0,
  284. parent = check_replyto_id,
  285. type = 'virtual',
  286. description = 'Reply-To display name matches From',
  287. group = 'headers',
  288. }
  289. rspamd_config:register_symbol{
  290. name = 'REPLYTO_EMAIL_HAS_TITLE',
  291. score = 2.0,
  292. parent = check_replyto_id,
  293. type = 'virtual',
  294. description = 'Reply-To header has title',
  295. group = 'headers',
  296. }
  297. rspamd_config:register_symbol{
  298. name = 'REPLYTO_EQ_TO_ADDR',
  299. score = 5.0,
  300. parent = check_replyto_id,
  301. type = 'virtual',
  302. description = 'Reply-To is the same as the To address',
  303. group = 'headers',
  304. }
  305. rspamd_config:register_dependency('CHECK_REPLYTO', 'CHECK_FROM')
  306. local check_mime_id = rspamd_config:register_symbol{
  307. name = 'CHECK_MIME',
  308. type = 'callback',
  309. callback = function(task)
  310. local parts = task:get_parts()
  311. if not parts then return false end
  312. -- Make sure there is a MIME-Version header
  313. local mv = task:get_header('MIME-Version')
  314. local missing_mime = false
  315. if (not mv) then
  316. missing_mime = true
  317. end
  318. local found_ma = false
  319. local found_plain = false
  320. local found_html = false
  321. local cte_7bit = false
  322. for _,p in ipairs(parts) do
  323. local mtype,subtype = p:get_type()
  324. local ctype = mtype:lower() .. '/' .. subtype:lower()
  325. if (ctype == 'multipart/alternative') then
  326. found_ma = true
  327. end
  328. if (ctype == 'text/plain') then
  329. if p:get_cte() == '7bit' then
  330. cte_7bit = true
  331. end
  332. found_plain = true
  333. end
  334. if (ctype == 'text/html') then
  335. if p:get_cte() == '7bit' then
  336. cte_7bit = true
  337. end
  338. found_html = true
  339. end
  340. end
  341. if missing_mime then
  342. if not (not found_ma and ((found_plain or found_html) and cte_7bit)) then
  343. task:insert_result('MISSING_MIME_VERSION', 1.0)
  344. end
  345. end
  346. if (found_ma) then
  347. if (not found_plain) then
  348. task:insert_result('MIME_MA_MISSING_TEXT', 1.0)
  349. end
  350. if (not found_html) then
  351. task:insert_result('MIME_MA_MISSING_HTML', 1.0)
  352. end
  353. end
  354. end
  355. }
  356. rspamd_config:register_symbol{
  357. name = 'MISSING_MIME_VERSION',
  358. score = 2.0,
  359. parent = check_mime_id,
  360. type = 'virtual',
  361. description = 'MIME-Version header is missing',
  362. group = 'headers',
  363. }
  364. rspamd_config:register_symbol{
  365. name = 'MIME_MA_MISSING_TEXT',
  366. score = 2.0,
  367. parent = check_mime_id,
  368. type = 'virtual',
  369. description = 'MIME multipart/alternative missing text/plain part',
  370. group = 'headers',
  371. }
  372. rspamd_config:register_symbol{
  373. name = 'MIME_MA_MISSING_HTML',
  374. score = 1.0,
  375. parent = check_mime_id,
  376. type = 'virtual',
  377. description = 'MIME multipart/alternative missing text/html part',
  378. group = 'headers',
  379. }
  380. -- Used to be called IS_LIST
  381. rspamd_config.PREVIOUSLY_DELIVERED = {
  382. callback = function(task)
  383. if not task:has_recipients(2) then return false end
  384. local to = task:get_recipients(2)
  385. local rcvds = task:get_header_full('Received')
  386. if not rcvds then return false end
  387. for _, rcvd in ipairs(rcvds) do
  388. local _,_,addr = rcvd['decoded']:lower():find("%sfor%s<(.-)>")
  389. if addr then
  390. for _, toa in ipairs(to) do
  391. if toa and toa.addr:lower() == addr then
  392. return true, addr
  393. end
  394. end
  395. return false
  396. end
  397. end
  398. end,
  399. description = 'Message either to a list or was forwarded',
  400. score = 0.0
  401. }
  402. rspamd_config.BROKEN_HEADERS = {
  403. callback = function(task)
  404. return task:has_flag('broken_headers')
  405. end,
  406. score = 10.0,
  407. group = 'headers',
  408. description = 'Headers structure is likely broken'
  409. }
  410. rspamd_config.BROKEN_CONTENT_TYPE = {
  411. callback = function(task)
  412. return fun.any(function(p) return p:is_broken() end,
  413. task:get_parts())
  414. end,
  415. score = 1.5,
  416. group = 'headers',
  417. description = 'Message has part with broken content type'
  418. }
  419. rspamd_config.HEADER_RCONFIRM_MISMATCH = {
  420. callback = function (task)
  421. local header_from = nil
  422. local cread = task:get_header('X-Confirm-Reading-To')
  423. if task:has_from('mime') then
  424. header_from = task:get_from('mime')[1]
  425. end
  426. local header_cread = nil
  427. if cread then
  428. local headers_cread = util.parse_mail_address(cread, task:get_mempool())
  429. if headers_cread then header_cread = headers_cread[1] end
  430. end
  431. if header_from and header_cread then
  432. if not string.find(header_from['addr'], header_cread['addr']) then
  433. return true
  434. end
  435. end
  436. return false
  437. end,
  438. score = 2.0,
  439. group = 'headers',
  440. description = 'Read confirmation address is different to from address'
  441. }
  442. rspamd_config.HEADER_FORGED_MDN = {
  443. callback = function (task)
  444. local mdn = task:get_header('Disposition-Notification-To')
  445. if not mdn then return false end
  446. local header_rp = nil
  447. if task:has_from('smtp') then
  448. header_rp = task:get_from('smtp')[1]
  449. end
  450. -- Parse mail addr
  451. local headers_mdn = util.parse_mail_address(mdn, task:get_mempool())
  452. if headers_mdn and not header_rp then return true end
  453. if header_rp and not headers_mdn then return false end
  454. if not headers_mdn and not header_rp then return false end
  455. local found_match = false
  456. for _, h in ipairs(headers_mdn) do
  457. if util.strequal_caseless(h['addr'], header_rp['addr']) then
  458. found_match = true
  459. break
  460. end
  461. end
  462. return (not found_match)
  463. end,
  464. score = 2.0,
  465. group = 'headers',
  466. description = 'Read confirmation address is different to return path'
  467. }
  468. local headers_unique = {
  469. 'Content-Type',
  470. 'Content-Transfer-Encoding',
  471. -- https://tools.ietf.org/html/rfc5322#section-3.6
  472. 'Date',
  473. 'From',
  474. 'Sender',
  475. 'Reply-To',
  476. 'To',
  477. 'Cc',
  478. 'Bcc',
  479. 'Message-ID',
  480. 'In-Reply-To',
  481. 'References',
  482. 'Subject'
  483. }
  484. rspamd_config.MULTIPLE_UNIQUE_HEADERS = {
  485. callback = function(task)
  486. local res = 0
  487. local res_tbl = {}
  488. for _,hdr in ipairs(headers_unique) do
  489. local h = task:get_header_full(hdr)
  490. if h and #h > 1 then
  491. res = res + 1
  492. table.insert(res_tbl, hdr)
  493. end
  494. end
  495. if res > 0 then
  496. return true,res,table.concat(res_tbl, ',')
  497. end
  498. return false
  499. end,
  500. score = 5.0,
  501. group = 'headers',
  502. one_shot = true,
  503. description = 'Repeated unique headers'
  504. }
  505. rspamd_config.MISSING_FROM = {
  506. callback = function(task)
  507. local from = task:get_header('From')
  508. if from == nil or from == '' then
  509. return true
  510. end
  511. return false
  512. end,
  513. score = 2.0,
  514. group = 'headers',
  515. description = 'Missing From: header'
  516. }
  517. rspamd_config.MV_CASE = {
  518. callback = function (task)
  519. local mv = task:get_header('Mime-Version', true)
  520. if (mv) then return true end
  521. end,
  522. description = 'Mime-Version .vs. MIME-Version',
  523. score = 0.5,
  524. group = 'headers',
  525. }
  526. rspamd_config.FAKE_REPLY = {
  527. callback = function (task)
  528. local subject = task:get_header('Subject')
  529. if (subject and subject:lower():find('^re:')) then
  530. local ref = task:get_header('References')
  531. local rt = task:get_header('In-Reply-To')
  532. if (not (ref or rt)) then return true end
  533. end
  534. return false
  535. end,
  536. description = 'Fake reply',
  537. score = 1.0,
  538. group = 'headers'
  539. }
  540. local check_from_id = rspamd_config:register_symbol{
  541. name = 'CHECK_FROM',
  542. type = 'callback',
  543. callback = function(task)
  544. local envfrom = task:get_from(1)
  545. local from = task:get_from(2)
  546. if (from and from[1] and (from[1].name == nil or from[1].name == '' )) then
  547. task:insert_result('FROM_NO_DN', 1.0)
  548. elseif (from and from[1] and from[1].name and
  549. util.strequal_caseless(from[1].name, from[1].addr)) then
  550. task:insert_result('FROM_DN_EQ_ADDR', 1.0)
  551. elseif (from and from[1] and from[1].name and from[1].name ~= '') then
  552. task:insert_result('FROM_HAS_DN', 1.0)
  553. -- Look for Mr/Mrs/Dr titles
  554. local n = from[1].name:lower()
  555. local match, match_end
  556. match, match_end = n:find('^mrs?[%.%s]')
  557. if match then
  558. task:insert_result('FROM_NAME_HAS_TITLE', 1.0, n:sub(match, match_end-1))
  559. end
  560. match, match_end = n:find('^dr[%.%s]')
  561. if match then
  562. task:insert_result('FROM_NAME_HAS_TITLE', 1.0, n:sub(match, match_end-1))
  563. end
  564. -- Check for excess spaces
  565. if n:find('%s%s') then
  566. task:insert_result('FROM_NAME_EXCESS_SPACE', 1.0)
  567. end
  568. end
  569. if (envfrom and from and envfrom[1] and from[1] and
  570. util.strequal_caseless(envfrom[1].addr, from[1].addr))
  571. then
  572. task:insert_result('FROM_EQ_ENVFROM', 1.0)
  573. elseif (envfrom and envfrom[1] and envfrom[1].addr) then
  574. task:insert_result('FROM_NEQ_ENVFROM', 1.0, ((from or E)[1] or E).addr or '', envfrom[1].addr)
  575. end
  576. local to = task:get_recipients(2)
  577. if not (to and to[1] and #to == 1 and from and from[1]) then return false end
  578. -- Check if FROM == TO
  579. if (util.strequal_caseless(to[1].addr, from[1].addr)) then
  580. task:insert_result('TO_EQ_FROM', 1.0)
  581. elseif (to[1].domain and from[1].domain and
  582. util.strequal_caseless(to[1].domain, from[1].domain))
  583. then
  584. task:insert_result('TO_DOM_EQ_FROM_DOM', 1.0)
  585. end
  586. end
  587. }
  588. rspamd_config:register_symbol{
  589. name = 'FROM_NO_DN',
  590. score = 0,
  591. group = 'headers',
  592. parent = check_from_id,
  593. type = 'virtual',
  594. description = 'From header does not have a display name',
  595. }
  596. rspamd_config:register_symbol{
  597. name = 'FROM_DN_EQ_ADDR',
  598. score = 1.0,
  599. group = 'headers',
  600. parent = check_from_id,
  601. type = 'virtual',
  602. description = 'From header display name is the same as the address',
  603. }
  604. rspamd_config:register_symbol{
  605. name = 'FROM_HAS_DN',
  606. score = 0.0,
  607. group = 'headers',
  608. parent = check_from_id,
  609. type = 'virtual',
  610. description = 'From header has a display name',
  611. }
  612. rspamd_config:register_symbol{
  613. name = 'FROM_NAME_EXCESS_SPACE',
  614. score = 1.0,
  615. group = 'headers',
  616. parent = check_from_id,
  617. type = 'virtual',
  618. description = 'From header display name contains excess whitespace',
  619. }
  620. rspamd_config:register_symbol{
  621. name = 'FROM_NAME_HAS_TITLE',
  622. score = 1.0,
  623. group = 'headers',
  624. parent = check_from_id,
  625. type = 'virtual',
  626. description = 'From header display name has a title (Mr/Mrs/Dr)',
  627. }
  628. rspamd_config:register_symbol{
  629. name = 'FROM_EQ_ENVFROM',
  630. score = 0.0,
  631. group = 'headers',
  632. parent = check_from_id,
  633. type = 'virtual',
  634. description = 'From address is the same as the envelope',
  635. }
  636. rspamd_config:register_symbol{
  637. name = 'FROM_NEQ_ENVFROM',
  638. score = 0.0,
  639. group = 'headers',
  640. parent = check_from_id,
  641. type = 'virtual',
  642. description = 'From address is different to the envelope',
  643. }
  644. rspamd_config:register_symbol{
  645. name = 'TO_EQ_FROM',
  646. score = 0.0,
  647. group = 'headers',
  648. parent = check_from_id,
  649. type = 'virtual',
  650. description = 'To address matches the From address',
  651. }
  652. rspamd_config:register_symbol{
  653. name = 'TO_DOM_EQ_FROM_DOM',
  654. score = 0.0,
  655. group = 'headers',
  656. parent = check_from_id,
  657. type = 'virtual',
  658. description = 'To domain is the same as the From domain',
  659. }
  660. local check_to_cc_id = rspamd_config:register_symbol{
  661. name = 'CHECK_TO_CC',
  662. type = 'callback',
  663. callback = function(task)
  664. local rcpts = task:get_recipients(1)
  665. local to = task:get_recipients(2)
  666. local to_match_envrcpt = 0
  667. local cnts = {
  668. [1] = 'ONE',
  669. [2] = 'TWO',
  670. [3] = 'THREE',
  671. [5] = 'FIVE',
  672. [7] = 'SEVEN',
  673. [12] = 'TWELVE',
  674. [50] = 'GT_50'
  675. }
  676. local def = 'ZERO'
  677. if (not to) then return false end
  678. -- Add symbol for recipient count
  679. local nrcpt = #to
  680. for k,v in pairs(cnts) do
  681. if nrcpt >= tonumber(k) then
  682. def = v
  683. end
  684. end
  685. task:insert_result('RCPT_COUNT_' .. def, 1.0, tostring(nrcpt))
  686. -- Check for display names
  687. local to_dn_count = 0
  688. local to_dn_eq_addr_count = 0
  689. for _, toa in ipairs(to) do
  690. -- To: Recipients <noreply@dropbox.com>
  691. if (toa['name'] and (toa['name']:lower() == 'recipient'
  692. or toa['name']:lower() == 'recipients')) then
  693. task:insert_result('TO_DN_RECIPIENTS', 1.0)
  694. end
  695. if (toa['name'] and util.strequal_caseless(toa['name'], toa['addr'])) then
  696. to_dn_eq_addr_count = to_dn_eq_addr_count + 1
  697. elseif (toa['name'] and toa['name'] ~= '') then
  698. to_dn_count = to_dn_count + 1
  699. end
  700. -- See if header recipients match envrcpts
  701. if (rcpts) then
  702. for _, rcpt in ipairs(rcpts) do
  703. if (toa and toa['addr'] and rcpt and rcpt['addr'] and
  704. util.strequal_caseless(rcpt['addr'], toa['addr']))
  705. then
  706. to_match_envrcpt = to_match_envrcpt + 1
  707. end
  708. end
  709. end
  710. end
  711. if (to_dn_count == 0 and to_dn_eq_addr_count == 0) then
  712. task:insert_result('TO_DN_NONE', 1.0)
  713. elseif (to_dn_count == #to) then
  714. task:insert_result('TO_DN_ALL', 1.0)
  715. elseif (to_dn_count > 0) then
  716. task:insert_result('TO_DN_SOME', 1.0)
  717. end
  718. if (to_dn_eq_addr_count == #to) then
  719. task:insert_result('TO_DN_EQ_ADDR_ALL', 1.0)
  720. elseif (to_dn_eq_addr_count > 0) then
  721. task:insert_result('TO_DN_EQ_ADDR_SOME', 1.0)
  722. end
  723. -- See if header recipients match envelope recipients
  724. if (to_match_envrcpt == #to) then
  725. task:insert_result('TO_MATCH_ENVRCPT_ALL', 1.0)
  726. elseif (to_match_envrcpt > 0) then
  727. task:insert_result('TO_MATCH_ENVRCPT_SOME', 1.0)
  728. end
  729. end
  730. }
  731. rspamd_config:register_symbol{
  732. name = 'RCPT_COUNT_ZERO',
  733. score = 0.0,
  734. parent = check_to_cc_id,
  735. type = 'virtual',
  736. description = 'No recipients',
  737. group = 'headers',
  738. }
  739. rspamd_config:register_symbol{
  740. name = 'RCPT_COUNT_ONE',
  741. score = 0.0,
  742. parent = check_to_cc_id,
  743. type = 'virtual',
  744. description = 'One recipient',
  745. group = 'headers',
  746. }
  747. rspamd_config:register_symbol{
  748. name = 'RCPT_COUNT_TWO',
  749. score = 0.0,
  750. parent = check_to_cc_id,
  751. type = 'virtual',
  752. description = 'Two recipients',
  753. group = 'headers',
  754. }
  755. rspamd_config:register_symbol{
  756. name = 'RCPT_COUNT_THREE',
  757. score = 0.0,
  758. parent = check_to_cc_id,
  759. type = 'virtual',
  760. description = '3-5 recipients',
  761. group = 'headers',
  762. }
  763. rspamd_config:register_symbol{
  764. name = 'RCPT_COUNT_FIVE',
  765. score = 0.0,
  766. parent = check_to_cc_id,
  767. type = 'virtual',
  768. description = '5-7 recipients',
  769. group = 'headers',
  770. }
  771. rspamd_config:register_symbol{
  772. name = 'RCPT_COUNT_SEVEN',
  773. score = 0.0,
  774. parent = check_to_cc_id,
  775. type = 'virtual',
  776. description = '7-11 recipients',
  777. group = 'headers',
  778. }
  779. rspamd_config:register_symbol{
  780. name = 'RCPT_COUNT_TWELVE',
  781. score = 0.0,
  782. parent = check_to_cc_id,
  783. type = 'virtual',
  784. description = '12-50 recipients',
  785. group = 'headers',
  786. }
  787. rspamd_config:register_symbol{
  788. name = 'RCPT_COUNT_GT_50',
  789. score = 0.0,
  790. parent = check_to_cc_id,
  791. type = 'virtual',
  792. description = '50+ recipients',
  793. group = 'headers',
  794. }
  795. rspamd_config:register_symbol{
  796. name = 'TO_DN_RECIPIENTS',
  797. score = 2.0,
  798. group = 'headers',
  799. parent = check_to_cc_id,
  800. type = 'virtual',
  801. description = 'To header display name is "Recipients"',
  802. }
  803. rspamd_config:register_symbol{
  804. name = 'TO_DN_NONE',
  805. score = 0.0,
  806. group = 'headers',
  807. parent = check_to_cc_id,
  808. type = 'virtual',
  809. description = 'None of the recipients have display names',
  810. }
  811. rspamd_config:register_symbol{
  812. name = 'TO_DN_ALL',
  813. score = 0.0,
  814. group = 'headers',
  815. parent = check_to_cc_id,
  816. type = 'virtual',
  817. description = 'All the recipients have display names',
  818. }
  819. rspamd_config:register_symbol{
  820. name = 'TO_DN_SOME',
  821. score = 0.0,
  822. group = 'headers',
  823. parent = check_to_cc_id,
  824. type = 'virtual',
  825. description = 'Some of the recipients have display names',
  826. }
  827. rspamd_config:register_symbol{
  828. name = 'TO_DN_EQ_ADDR_ALL',
  829. score = 0.0,
  830. group = 'headers',
  831. parent = check_to_cc_id,
  832. type = 'virtual',
  833. description = 'All of the recipients have display names that are the same as their address',
  834. }
  835. rspamd_config:register_symbol{
  836. name = 'TO_DN_EQ_ADDR_SOME',
  837. score = 0.0,
  838. group = 'headers',
  839. parent = check_to_cc_id,
  840. type = 'virtual',
  841. description = 'Some of the recipients have display names that are the same as their address',
  842. }
  843. rspamd_config:register_symbol{
  844. name = 'TO_MATCH_ENVRCPT_ALL',
  845. score = 0.0,
  846. group = 'headers',
  847. parent = check_to_cc_id,
  848. type = 'virtual',
  849. description = 'All of the recipients match the envelope',
  850. }
  851. rspamd_config:register_symbol{
  852. name = 'TO_MATCH_ENVRCPT_SOME',
  853. score = 0.0,
  854. group = 'headers',
  855. parent = check_to_cc_id,
  856. type = 'virtual',
  857. description = 'Some of the recipients match the envelope',
  858. }
  859. rspamd_config.CTYPE_MISSING_DISPOSITION = {
  860. callback = function(task)
  861. local parts = task:get_parts()
  862. if (not parts) or (parts and #parts < 1) then return false end
  863. for _,p in ipairs(parts) do
  864. local ct = p:get_header('Content-Type')
  865. if (ct and ct:lower():match('^application/octet%-stream') ~= nil) then
  866. local cd = p:get_header('Content-Disposition')
  867. if (not cd) or (cd and cd:lower():find('^attachment') == nil) then
  868. local ci = p:get_header('Content-ID')
  869. if ci or (#parts > 1 and (cd and cd:find('filename=.+%.asc') ~= nil))
  870. then
  871. return false
  872. end
  873. return true
  874. end
  875. end
  876. end
  877. return false
  878. end,
  879. description = 'Binary content-type not specified as an attachment',
  880. score = 4.0,
  881. group = 'headers'
  882. }
  883. rspamd_config.CTYPE_MIXED_BOGUS = {
  884. callback = function(task)
  885. local ct = task:get_header('Content-Type')
  886. if (not ct) then return false end
  887. local parts = task:get_parts()
  888. if (not parts) then return false end
  889. if (not ct:lower():match('^multipart/mixed')) then return false end
  890. local found = false
  891. -- Check each part and look for a part that isn't multipart/* or text/plain or text/html
  892. for _,p in ipairs(parts) do
  893. local pct = p:get_header('Content-Type')
  894. if (pct) then
  895. pct = pct:lower()
  896. if not ((pct:match('^multipart/') or
  897. pct:match('^text/plain') or
  898. pct:match('^text/html'))) then
  899. found = true
  900. end
  901. end
  902. end
  903. if (not found) then return true end
  904. return false
  905. end,
  906. description = 'multipart/mixed without non-textual part',
  907. score = 1.0,
  908. group = 'headers'
  909. }
  910. local function check_for_base64_text(part)
  911. local ct = part:get_header('Content-Type')
  912. if (not ct) then return false end
  913. ct = ct:lower()
  914. if (ct:match('^text')) then
  915. -- Check encoding
  916. local cte = part:get_header('Content-Transfer-Encoding')
  917. if (cte and cte:lower():match('^base64')) then
  918. return true
  919. end
  920. end
  921. return false
  922. end
  923. rspamd_config.MIME_BASE64_TEXT = {
  924. callback = function(task)
  925. -- Check outer part
  926. if (check_for_base64_text(task)) then
  927. return true
  928. else
  929. local parts = task:get_parts()
  930. if (not parts) then return false end
  931. -- Check each part and look for base64 encoded text parts
  932. for _, part in ipairs(parts) do
  933. if (check_for_base64_text(part)) then
  934. return true
  935. end
  936. end
  937. end
  938. return false
  939. end,
  940. description = 'Has text part encoded in base64',
  941. score = 0.1,
  942. group = 'headers'
  943. }
  944. local function is_8bit_addr(addr)
  945. if addr.flags and addr.flags['8bit'] then
  946. return true
  947. end
  948. return false;
  949. end
  950. rspamd_config.INVALID_FROM_8BIT = {
  951. callback = function(task)
  952. local from = (task:get_from('mime') or {})[1] or {}
  953. if is_8bit_addr(from) then
  954. return true
  955. end
  956. return false
  957. end,
  958. description = 'Invalid 8bit character in From header',
  959. score = 6.0,
  960. group = 'headers'
  961. }
  962. rspamd_config.INVALID_RCPT_8BIT = {
  963. callback = function(task)
  964. local rcpts = task:get_recipients('mime') or {}
  965. return fun.any(function(rcpt)
  966. if is_8bit_addr(rcpt) then
  967. return true
  968. end
  969. return false
  970. end, rcpts)
  971. end,
  972. description = 'Invalid 8bit character in recipients headers',
  973. score = 6.0,
  974. group = 'headers'
  975. }
  976. rspamd_config.XM_CASE = {
  977. callback = function (task)
  978. local xm = task:get_header('X-mailer', true)
  979. if (xm) then return true end
  980. end,
  981. description = 'X-mailer .vs. X-Mailer',
  982. score = 0.5,
  983. group = 'headers',
  984. }