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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963
  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 = 'header',
  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 = 'header',
  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 = 'header',
  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 = 'header',
  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 = 'header',
  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 = 'header',
  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 = 'header',
  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 = 'header',
  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 = 'header',
  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 = 'header',
  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 = 'header',
  168. }
  169. local function get_raw_header(task, name)
  170. return ((task:get_header_full(name) or {})[1] or {})['raw']
  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)
  177. if not (rt and rt[1]) then
  178. task:insert_result('REPLYTO_UNPARSEABLE', 1.0)
  179. return false
  180. else
  181. task:insert_result('HAS_REPLYTO', 1.0)
  182. local rta = rt[1].addr
  183. if rta then
  184. -- Check if Reply-To address starts with title seen in display name
  185. local sym = task:get_symbol('FROM_NAME_HAS_TITLE')
  186. local title = (((sym or E)[1] or E).options or E)[1]
  187. if title then
  188. rta = rta:lower()
  189. if rta:find('^' .. title) then
  190. task:insert_result('REPLYTO_EMAIL_HAS_TITLE', 1.0)
  191. end
  192. end
  193. end
  194. end
  195. -- See if Reply-To matches From in some way
  196. local from = task:get_from(2)
  197. local from_h = get_raw_header(task, 'From')
  198. if not (from and from[1]) then return false end
  199. if (from_h and from_h == replyto) then
  200. -- From and Reply-To are identical
  201. task:insert_result('REPLYTO_EQ_FROM', 1.0)
  202. else
  203. if (from and from[1]) then
  204. -- See if From and Reply-To addresses match
  205. if (util.strequal_caseless(from[1].addr, rt[1].addr)) then
  206. task:insert_result('REPLYTO_ADDR_EQ_FROM', 1.0)
  207. elseif from[1].domain and rt[1].domain then
  208. if (util.strequal_caseless(from[1].domain, rt[1].domain)) then
  209. task:insert_result('REPLYTO_DOM_EQ_FROM_DOM', 1.0)
  210. else
  211. task:insert_result('REPLYTO_DOM_NEQ_FROM_DOM', 1.0)
  212. end
  213. end
  214. -- See if the Display Names match
  215. if (from[1].name and rt[1].name and
  216. util.strequal_caseless(from[1].name, rt[1].name)) then
  217. task:insert_result('REPLYTO_DN_EQ_FROM_DN', 1.0)
  218. end
  219. end
  220. end
  221. end
  222. )
  223. rspamd_config:register_symbol{
  224. name = 'REPLYTO_UNPARSEABLE',
  225. score = 1.0,
  226. parent = check_replyto_id,
  227. type = 'virtual',
  228. description = 'Reply-To header could not be parsed',
  229. group = 'header',
  230. }
  231. rspamd_config:register_symbol{
  232. name = 'HAS_REPLYTO',
  233. score = 0.0,
  234. parent = check_replyto_id,
  235. type = 'virtual',
  236. description = 'Has Reply-To header',
  237. group = 'header',
  238. }
  239. rspamd_config:register_symbol{
  240. name = 'REPLYTO_EQ_FROM',
  241. score = 0.0,
  242. parent = check_replyto_id,
  243. type = 'virtual',
  244. description = 'Reply-To header is identical to From header',
  245. group = 'header',
  246. }
  247. rspamd_config:register_symbol{
  248. name = 'REPLYTO_ADDR_EQ_FROM',
  249. score = 0.0,
  250. parent = check_replyto_id,
  251. type = 'virtual',
  252. description = 'Reply-To header is identical to SMTP From',
  253. group = 'header',
  254. }
  255. rspamd_config:register_symbol{
  256. name = 'REPLYTO_DOM_EQ_FROM_DOM',
  257. score = 0.0,
  258. parent = check_replyto_id,
  259. type = 'virtual',
  260. description = 'Reply-To domain matches the From domain',
  261. group = 'header',
  262. }
  263. rspamd_config:register_symbol{
  264. name = 'REPLYTO_DOM_NEQ_FROM_DOM',
  265. score = 0.0,
  266. parent = check_replyto_id,
  267. type = 'virtual',
  268. description = 'Reply-To domain does not match the From domain',
  269. group = 'header',
  270. }
  271. rspamd_config:register_symbol{
  272. name = 'REPLYTO_DN_EQ_FROM_DN',
  273. score = 0.0,
  274. parent = check_replyto_id,
  275. type = 'virtual',
  276. description = 'Reply-To display name matches From',
  277. group = 'header',
  278. }
  279. rspamd_config:register_symbol{
  280. name = 'REPLYTO_EMAIL_HAS_TITLE',
  281. score = 2.0,
  282. parent = check_replyto_id,
  283. type = 'virtual',
  284. description = 'Reply-To header has title',
  285. group = 'header',
  286. }
  287. rspamd_config:register_dependency(check_replyto_id, 'FROM_NAME_HAS_TITLE')
  288. local check_mime_id = rspamd_config:register_symbol{
  289. name = 'CHECK_MIME',
  290. type = 'callback',
  291. callback = function(task)
  292. local parts = task:get_parts()
  293. if not parts then return false end
  294. -- Make sure there is a MIME-Version header
  295. local mv = task:get_header('MIME-Version')
  296. local missing_mime = false
  297. if (not mv) then
  298. missing_mime = true
  299. end
  300. local found_ma = false
  301. local found_plain = false
  302. local found_html = false
  303. local cte_7bit = false
  304. for _,p in ipairs(parts) do
  305. local mtype,subtype = p:get_type()
  306. local ctype = mtype:lower() .. '/' .. subtype:lower()
  307. if (ctype == 'multipart/alternative') then
  308. found_ma = true
  309. end
  310. if (ctype == 'text/plain') then
  311. if p:get_cte() == '7bit' then
  312. cte_7bit = true
  313. end
  314. found_plain = true
  315. end
  316. if (ctype == 'text/html') then
  317. if p:get_cte() == '7bit' then
  318. cte_7bit = true
  319. end
  320. found_html = true
  321. end
  322. end
  323. if missing_mime then
  324. if not (not found_ma and ((found_plain or found_html) and cte_7bit)) then
  325. task:insert_result('MISSING_MIME_VERSION', 1.0)
  326. end
  327. end
  328. if (found_ma) then
  329. if (not found_plain) then
  330. task:insert_result('MIME_MA_MISSING_TEXT', 1.0)
  331. end
  332. if (not found_html) then
  333. task:insert_result('MIME_MA_MISSING_HTML', 1.0)
  334. end
  335. end
  336. end
  337. }
  338. rspamd_config:register_symbol{
  339. name = 'MISSING_MIME_VERSION',
  340. score = 2.0,
  341. parent = check_mime_id,
  342. type = 'virtual',
  343. description = 'MIME-Version header is missing',
  344. group = 'header',
  345. }
  346. rspamd_config:register_symbol{
  347. name = 'MIME_MA_MISSING_TEXT',
  348. score = 2.0,
  349. parent = check_mime_id,
  350. type = 'virtual',
  351. description = 'MIME multipart/alternative missing text/plain part',
  352. group = 'header',
  353. }
  354. rspamd_config:register_symbol{
  355. name = 'MIME_MA_MISSING_HTML',
  356. score = 1.0,
  357. parent = check_mime_id,
  358. type = 'virtual',
  359. description = 'MIME multipart/alternative missing text/html part',
  360. group = 'header',
  361. }
  362. -- Used to be called IS_LIST
  363. rspamd_config.PREVIOUSLY_DELIVERED = {
  364. callback = function(task)
  365. if not task:has_recipients(2) then return false end
  366. local to = task:get_recipients(2)
  367. local rcvds = task:get_header_full('Received')
  368. if not rcvds then return false end
  369. for _, rcvd in ipairs(rcvds) do
  370. local _,_,addr = rcvd['decoded']:lower():find("%sfor%s<(.-)>")
  371. if addr then
  372. for _, toa in ipairs(to) do
  373. if toa and toa.addr:lower() == addr then
  374. return true, addr
  375. end
  376. end
  377. return false
  378. end
  379. end
  380. end,
  381. description = 'Message either to a list or was forwarded',
  382. score = 0.0
  383. }
  384. rspamd_config.BROKEN_HEADERS = {
  385. callback = function(task)
  386. return task:has_flag('broken_headers')
  387. end,
  388. score = 10.0,
  389. group = 'header',
  390. description = 'Headers structure is likely broken'
  391. }
  392. rspamd_config.BROKEN_CONTENT_TYPE = {
  393. callback = function(task)
  394. return fun.any(function(p) return p:is_broken() end,
  395. task:get_parts())
  396. end,
  397. score = 1.5,
  398. group = 'header',
  399. description = 'Message has part with broken content type'
  400. }
  401. rspamd_config.HEADER_RCONFIRM_MISMATCH = {
  402. callback = function (task)
  403. local header_from = nil
  404. local cread = task:get_header('X-Confirm-Reading-To')
  405. if task:has_from('mime') then
  406. header_from = task:get_from('mime')[1]
  407. end
  408. local header_cread = nil
  409. if cread then
  410. local headers_cread = util.parse_mail_address(cread)
  411. if headers_cread then header_cread = headers_cread[1] end
  412. end
  413. if header_from and header_cread then
  414. if not string.find(header_from['addr'], header_cread['addr']) then
  415. return true
  416. end
  417. end
  418. return false
  419. end,
  420. score = 2.0,
  421. group = 'header',
  422. description = 'Read confirmation address is different to from address'
  423. }
  424. rspamd_config.HEADER_FORGED_MDN = {
  425. callback = function (task)
  426. local mdn = task:get_header('Disposition-Notification-To')
  427. if not mdn then return false end
  428. local header_rp = nil
  429. if task:has_from('smtp') then
  430. header_rp = task:get_from('smtp')[1]
  431. end
  432. -- Parse mail addr
  433. local headers_mdn = util.parse_mail_address(mdn)
  434. if headers_mdn and not header_rp then return true end
  435. if header_rp and not headers_mdn then return false end
  436. if not headers_mdn and not header_rp then return false end
  437. local found_match = false
  438. for _, h in ipairs(headers_mdn) do
  439. if util.strequal_caseless(h['addr'], header_rp['addr']) then
  440. found_match = true
  441. break
  442. end
  443. end
  444. return (not found_match)
  445. end,
  446. score = 2.0,
  447. group = 'header',
  448. description = 'Read confirmation address is different to return path'
  449. }
  450. local headers_unique = {
  451. 'Content-Type',
  452. 'Content-Transfer-Encoding',
  453. -- https://tools.ietf.org/html/rfc5322#section-3.6
  454. 'Date',
  455. 'From',
  456. 'Sender',
  457. 'Reply-To',
  458. 'To',
  459. 'Cc',
  460. 'Bcc',
  461. 'Message-ID',
  462. 'In-Reply-To',
  463. 'References',
  464. 'Subject'
  465. }
  466. rspamd_config.MULTIPLE_UNIQUE_HEADERS = {
  467. callback = function(task)
  468. local res = 0
  469. local res_tbl = {}
  470. for _,hdr in ipairs(headers_unique) do
  471. local h = task:get_header_full(hdr)
  472. if h and #h > 1 then
  473. res = res + 1
  474. table.insert(res_tbl, hdr)
  475. end
  476. end
  477. if res > 0 then
  478. return true,res,table.concat(res_tbl, ',')
  479. end
  480. return false
  481. end,
  482. score = 5.0,
  483. group = 'header',
  484. description = 'Repeated unique headers'
  485. }
  486. rspamd_config.MISSING_FROM = {
  487. callback = function(task)
  488. local from = task:get_header('From')
  489. if from == nil or from == '' then
  490. return true
  491. end
  492. return false
  493. end,
  494. score = 2.0,
  495. group = 'header',
  496. description = 'Missing From: header'
  497. }
  498. rspamd_config.MV_CASE = {
  499. callback = function (task)
  500. local mv = task:get_header('Mime-Version', true)
  501. if (mv) then return true end
  502. end,
  503. description = 'Mime-Version .vs. MIME-Version',
  504. score = 0.5
  505. }
  506. rspamd_config.FAKE_REPLY = {
  507. callback = function (task)
  508. local subject = task:get_header('Subject')
  509. if (subject and subject:lower():find('^re:')) then
  510. local ref = task:get_header('References')
  511. local rt = task:get_header('In-Reply-To')
  512. if (not (ref or rt)) then return true end
  513. end
  514. return false
  515. end,
  516. description = 'Fake reply',
  517. score = 1.0
  518. }
  519. local check_from_id = rspamd_config:register_symbol{
  520. name = 'CHECK_FROM',
  521. type = 'callback',
  522. callback = function(task)
  523. local envfrom = task:get_from(1)
  524. local from = task:get_from(2)
  525. if (from and from[1] and (from[1].name == nil or from[1].name == '' )) then
  526. task:insert_result('FROM_NO_DN', 1.0)
  527. elseif (from and from[1] and from[1].name and
  528. util.strequal_caseless(from[1].name, from[1].addr)) then
  529. task:insert_result('FROM_DN_EQ_ADDR', 1.0)
  530. elseif (from and from[1] and from[1].name and from[1].name ~= '') then
  531. task:insert_result('FROM_HAS_DN', 1.0)
  532. -- Look for Mr/Mrs/Dr titles
  533. local n = from[1].name:lower()
  534. local match, match_end
  535. match, match_end = n:find('^mrs?[%.%s]')
  536. if match then
  537. task:insert_result('FROM_NAME_HAS_TITLE', 1.0, n:sub(match, match_end-1))
  538. end
  539. match, match_end = n:find('^dr[%.%s]')
  540. if match then
  541. task:insert_result('FROM_NAME_HAS_TITLE', 1.0, n:sub(match, match_end-1))
  542. end
  543. -- Check for excess spaces
  544. if n:find('%s%s') then
  545. task:insert_result('FROM_NAME_EXCESS_SPACE', 1.0)
  546. end
  547. end
  548. if (envfrom and from and envfrom[1] and from[1] and
  549. util.strequal_caseless(envfrom[1].addr, from[1].addr))
  550. then
  551. task:insert_result('FROM_EQ_ENVFROM', 1.0)
  552. elseif (envfrom and envfrom[1] and envfrom[1].addr) then
  553. task:insert_result('FROM_NEQ_ENVFROM', 1.0, from and from[1].addr or '', envfrom[1].addr)
  554. end
  555. local to = task:get_recipients(2)
  556. if not (to and to[1] and #to == 1 and from) then return false end
  557. -- Check if FROM == TO
  558. if (util.strequal_caseless(to[1].addr, from[1].addr)) then
  559. task:insert_result('TO_EQ_FROM', 1.0)
  560. elseif (to[1].domain and from[1].domain and
  561. util.strequal_caseless(to[1].domain, from[1].domain))
  562. then
  563. task:insert_result('TO_DOM_EQ_FROM_DOM', 1.0)
  564. end
  565. end
  566. }
  567. rspamd_config:register_symbol{
  568. name = 'FROM_NO_DN',
  569. score = 0,
  570. group = 'header',
  571. parent = check_from_id,
  572. type = 'virtual',
  573. description = 'From header does not have a display name',
  574. }
  575. rspamd_config:register_symbol{
  576. name = 'FROM_DN_EQ_ADDR',
  577. score = 1.0,
  578. group = 'header',
  579. parent = check_from_id,
  580. type = 'virtual',
  581. description = 'From header display name is the same as the address',
  582. }
  583. rspamd_config:register_symbol{
  584. name = 'FROM_HAS_DN',
  585. score = 0.0,
  586. group = 'header',
  587. parent = check_from_id,
  588. type = 'virtual',
  589. description = 'From header has a display name',
  590. }
  591. rspamd_config:register_symbol{
  592. name = 'FROM_NAME_EXCESS_SPACE',
  593. score = 1.0,
  594. group = 'header',
  595. parent = check_from_id,
  596. type = 'virtual',
  597. description = 'From header display name contains excess whitespace',
  598. }
  599. rspamd_config:register_symbol{
  600. name = 'FROM_NAME_HAS_TITLE',
  601. score = 1.0,
  602. group = 'header',
  603. parent = check_from_id,
  604. type = 'virtual',
  605. description = 'From header display name has a title (Mr/Mrs/Dr)',
  606. }
  607. rspamd_config:register_symbol{
  608. name = 'FROM_EQ_ENVFROM',
  609. score = 0.0,
  610. group = 'header',
  611. parent = check_from_id,
  612. type = 'virtual',
  613. description = 'From address is the same as the envelope',
  614. }
  615. rspamd_config:register_symbol{
  616. name = 'FROM_NEQ_ENVFROM',
  617. score = 0.0,
  618. group = 'header',
  619. parent = check_from_id,
  620. type = 'virtual',
  621. description = 'From address is different to the envelope',
  622. }
  623. rspamd_config:register_symbol{
  624. name = 'TO_EQ_FROM',
  625. score = 0.0,
  626. group = 'header',
  627. parent = check_from_id,
  628. type = 'virtual',
  629. description = 'To address matches the From address',
  630. }
  631. rspamd_config:register_symbol{
  632. name = 'TO_DOM_EQ_FROM_DOM',
  633. score = 0.0,
  634. group = 'header',
  635. parent = check_from_id,
  636. type = 'virtual',
  637. description = 'To domain is the same as the From domain',
  638. }
  639. local check_to_cc_id = rspamd_config:register_symbol{
  640. name = 'CHECK_TO_CC',
  641. type = 'callback',
  642. callback = function(task)
  643. local rcpts = task:get_recipients(1)
  644. local to = task:get_recipients(2)
  645. local to_match_envrcpt = 0
  646. local cnts = {
  647. [1] = 'ONE',
  648. [2] = 'TWO',
  649. [3] = 'THREE',
  650. [5] = 'FIVE',
  651. [7] = 'SEVEN',
  652. [12] = 'TWELVE',
  653. [50] = 'GT_50'
  654. }
  655. local def = 'ZERO'
  656. if (not to) then return false end
  657. -- Add symbol for recipient count
  658. local nrcpt = #to
  659. for k,v in pairs(cnts) do
  660. if nrcpt >= tonumber(k) then
  661. def = v
  662. end
  663. end
  664. task:insert_result('RCPT_COUNT_' .. def, 1.0, tostring(nrcpt))
  665. -- Check for display names
  666. local to_dn_count = 0
  667. local to_dn_eq_addr_count = 0
  668. for _, toa in ipairs(to) do
  669. -- To: Recipients <noreply@dropbox.com>
  670. if (toa['name'] and (toa['name']:lower() == 'recipient'
  671. or toa['name']:lower() == 'recipients')) then
  672. task:insert_result('TO_DN_RECIPIENTS', 1.0)
  673. end
  674. if (toa['name'] and util.strequal_caseless(toa['name'], toa['addr'])) then
  675. to_dn_eq_addr_count = to_dn_eq_addr_count + 1
  676. elseif (toa['name'] and toa['name'] ~= '') then
  677. to_dn_count = to_dn_count + 1
  678. end
  679. -- See if header recipients match envrcpts
  680. if (rcpts) then
  681. for _, rcpt in ipairs(rcpts) do
  682. if (toa and toa['addr'] and rcpt and rcpt['addr'] and
  683. util.strequal_caseless(rcpt['addr'], toa['addr']))
  684. then
  685. to_match_envrcpt = to_match_envrcpt + 1
  686. end
  687. end
  688. end
  689. end
  690. if (to_dn_count == 0 and to_dn_eq_addr_count == 0) then
  691. task:insert_result('TO_DN_NONE', 1.0)
  692. elseif (to_dn_count == #to) then
  693. task:insert_result('TO_DN_ALL', 1.0)
  694. elseif (to_dn_count > 0) then
  695. task:insert_result('TO_DN_SOME', 1.0)
  696. end
  697. if (to_dn_eq_addr_count == #to) then
  698. task:insert_result('TO_DN_EQ_ADDR_ALL', 1.0)
  699. elseif (to_dn_eq_addr_count > 0) then
  700. task:insert_result('TO_DN_EQ_ADDR_SOME', 1.0)
  701. end
  702. -- See if header recipients match envelope recipients
  703. if (to_match_envrcpt == #to) then
  704. task:insert_result('TO_MATCH_ENVRCPT_ALL', 1.0)
  705. elseif (to_match_envrcpt > 0) then
  706. task:insert_result('TO_MATCH_ENVRCPT_SOME', 1.0)
  707. end
  708. end
  709. }
  710. rspamd_config:register_symbol{
  711. name = 'RCPT_COUNT_ZERO',
  712. score = 0.0,
  713. parent = check_to_cc_id,
  714. type = 'virtual',
  715. description = 'No recipients',
  716. group = 'header',
  717. }
  718. rspamd_config:register_symbol{
  719. name = 'RCPT_COUNT_ONE',
  720. score = 0.0,
  721. parent = check_to_cc_id,
  722. type = 'virtual',
  723. description = 'One recipient',
  724. group = 'header',
  725. }
  726. rspamd_config:register_symbol{
  727. name = 'RCPT_COUNT_TWO',
  728. score = 0.0,
  729. parent = check_to_cc_id,
  730. type = 'virtual',
  731. description = 'Two recipients',
  732. group = 'header',
  733. }
  734. rspamd_config:register_symbol{
  735. name = 'RCPT_COUNT_THREE',
  736. score = 0.0,
  737. parent = check_to_cc_id,
  738. type = 'virtual',
  739. description = '3-5 recipients',
  740. group = 'header',
  741. }
  742. rspamd_config:register_symbol{
  743. name = 'RCPT_COUNT_FIVE',
  744. score = 0.0,
  745. parent = check_to_cc_id,
  746. type = 'virtual',
  747. description = '5-7 recipients',
  748. group = 'header',
  749. }
  750. rspamd_config:register_symbol{
  751. name = 'RCPT_COUNT_SEVEN',
  752. score = 0.0,
  753. parent = check_to_cc_id,
  754. type = 'virtual',
  755. description = '7-11 recipients',
  756. group = 'header',
  757. }
  758. rspamd_config:register_symbol{
  759. name = 'RCPT_COUNT_TWELVE',
  760. score = 0.0,
  761. parent = check_to_cc_id,
  762. type = 'virtual',
  763. description = '12-50 recipients',
  764. group = 'header',
  765. }
  766. rspamd_config:register_symbol{
  767. name = 'RCPT_COUNT_GT_50',
  768. score = 0.0,
  769. parent = check_to_cc_id,
  770. type = 'virtual',
  771. description = '50+ recipients',
  772. group = 'header',
  773. }
  774. rspamd_config:register_symbol{
  775. name = 'TO_DN_RECIPIENTS',
  776. score = 2.0,
  777. group = 'header',
  778. parent = check_to_cc_id,
  779. type = 'virtual',
  780. description = 'To header display name is "Recipients"',
  781. }
  782. rspamd_config:register_symbol{
  783. name = 'TO_DN_NONE',
  784. score = 0.0,
  785. group = 'header',
  786. parent = check_to_cc_id,
  787. type = 'virtual',
  788. description = 'None of the recipients have display names',
  789. }
  790. rspamd_config:register_symbol{
  791. name = 'TO_DN_ALL',
  792. score = 0.0,
  793. group = 'header',
  794. parent = check_to_cc_id,
  795. type = 'virtual',
  796. description = 'All the recipients have display names',
  797. }
  798. rspamd_config:register_symbol{
  799. name = 'TO_DN_SOME',
  800. score = 0.0,
  801. group = 'header',
  802. parent = check_to_cc_id,
  803. type = 'virtual',
  804. description = 'Some of the recipients have display names',
  805. }
  806. rspamd_config:register_symbol{
  807. name = 'TO_DN_EQ_ADDR_ALL',
  808. score = 0.0,
  809. group = 'header',
  810. parent = check_to_cc_id,
  811. type = 'virtual',
  812. description = 'All of the recipients have display names that are the same as their address',
  813. }
  814. rspamd_config:register_symbol{
  815. name = 'TO_DN_EQ_ADDR_SOME',
  816. score = 0.0,
  817. group = 'header',
  818. parent = check_to_cc_id,
  819. type = 'virtual',
  820. description = 'Some of the recipients have display names that are the same as their address',
  821. }
  822. rspamd_config:register_symbol{
  823. name = 'TO_MATCH_ENVRCPT_ALL',
  824. score = 0.0,
  825. group = 'header',
  826. parent = check_to_cc_id,
  827. type = 'virtual',
  828. description = 'All of the recipients match the envelope',
  829. }
  830. rspamd_config:register_symbol{
  831. name = 'TO_MATCH_ENVRCPT_SOME',
  832. score = 0.0,
  833. group = 'header',
  834. parent = check_to_cc_id,
  835. type = 'virtual',
  836. description = 'Some of the recipients match the envelope',
  837. }
  838. rspamd_config.CTYPE_MISSING_DISPOSITION = {
  839. callback = function(task)
  840. local parts = task:get_parts()
  841. if (not parts) or (parts and #parts < 1) then return false end
  842. for _,p in ipairs(parts) do
  843. local ct = p:get_header('Content-Type')
  844. if (ct and ct:lower():match('^application/octet%-stream') ~= nil) then
  845. local cd = p:get_header('Content-Disposition')
  846. if (not cd) or (cd and cd:lower():find('^attachment') == nil) then
  847. local ci = p:get_header('Content-ID')
  848. if ci then return false end
  849. return true
  850. end
  851. end
  852. end
  853. return false
  854. end,
  855. description = 'Binary content-type not specified as an attachment',
  856. score = 4.0,
  857. group = 'header'
  858. }
  859. rspamd_config.CTYPE_MIXED_BOGUS = {
  860. callback = function(task)
  861. local ct = task:get_header('Content-Type')
  862. if (not ct) then return false end
  863. local parts = task:get_parts()
  864. if (not parts) then return false end
  865. if (ct:lower():match('^multipart/mixed') ~= nil and #parts < 3)
  866. then
  867. return true, tostring(#parts)
  868. end
  869. return false
  870. end,
  871. description = 'multipart/mixed with less than 3 total parts',
  872. score = 0.1,
  873. group = 'headers'
  874. }
  875. local function is_8bit_addr(addr)
  876. if addr.flags and addr.flags['8bit'] then
  877. return true
  878. end
  879. return false;
  880. end
  881. rspamd_config.INVALID_FROM_8BIT = {
  882. callback = function(task)
  883. local from = (task:get_from('mime') or {})[1] or {}
  884. if is_8bit_addr(from) then
  885. return true
  886. end
  887. return false
  888. end,
  889. description = 'Invalid 8bit character in From header',
  890. score = 6.0,
  891. group = 'headers'
  892. }
  893. rspamd_config.INVALID_RCPT_8BIT = {
  894. callback = function(task)
  895. local rcpts = task:get_recipients('mime') or {}
  896. return fun.any(function(rcpt)
  897. if is_8bit_addr(rcpt) then
  898. return true
  899. end
  900. return false
  901. end, rcpts)
  902. end,
  903. description = 'Invalid 8bit character in recipients headers',
  904. score = 6.0,
  905. group = 'headers'
  906. }