Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

misc.lua 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783
  1. --[[
  2. Copyright (c) 2011-2015, Vsevolod Stakhov <vsevolod@highsecure.ru>
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. ]]--
  13. -- This is main lua config file for rspamd
  14. local util = require "rspamd_util"
  15. local rspamd_regexp = require "rspamd_regexp"
  16. -- Uncategorized rules
  17. local subject_re = rspamd_regexp.create('/^(?:(?:Re|Fwd|Fw|Aw|Antwort|Sv):\\s*)+(.+)$/i')
  18. -- Local functions
  19. -- Subject issues
  20. local function test_subject(task, check_function, rate)
  21. local function normalize_linear(a, x)
  22. local f = a * x
  23. return true, (( f < 1 ) and f or 1), tostring(x)
  24. end
  25. local sbj = task:get_header('Subject')
  26. if sbj then
  27. local stripped_subject = subject_re:search(sbj, false, true)
  28. if stripped_subject and stripped_subject[1] and stripped_subject[1][2] then
  29. sbj = stripped_subject[1][2]
  30. end
  31. local l = util.strlen_utf8(sbj)
  32. if check_function(sbj, l) then
  33. return normalize_linear(rate, l)
  34. end
  35. end
  36. return false
  37. end
  38. rspamd_config.SUBJ_ALL_CAPS = {
  39. callback = function(task)
  40. local caps_test = function(sbj)
  41. return util.is_uppercase(sbj)
  42. end
  43. return test_subject(task, caps_test, 1.0/40.0)
  44. end,
  45. score = 3.0,
  46. group = 'subject',
  47. description = 'All capital letters in subject'
  48. }
  49. rspamd_config.LONG_SUBJ = {
  50. callback = function(task)
  51. local length_test = function(_, len)
  52. return len > 200
  53. end
  54. return test_subject(task, length_test, 1.0/400.0)
  55. end,
  56. score = 3.0,
  57. group = 'subject',
  58. description = 'Subject is too long'
  59. }
  60. -- Different text parts
  61. rspamd_config.R_PARTS_DIFFER = {
  62. callback = function(task)
  63. local distance = task:get_mempool():get_variable('parts_distance', 'double')
  64. if distance then
  65. local nd = tonumber(distance)
  66. -- ND is relation of different words to total words
  67. if nd >= 0.5 then
  68. local tw = task:get_mempool():get_variable('total_words', 'int')
  69. if tw then
  70. local score
  71. if tw > 30 then
  72. -- We are confident about difference
  73. score = (nd - 0.5) * 2.0
  74. else
  75. -- We are not so confident about difference
  76. score = (nd - 0.5)
  77. end
  78. task:insert_result('R_PARTS_DIFFER', score,
  79. string.format('%.1f%%', tostring(100.0 * nd)))
  80. end
  81. end
  82. end
  83. return false
  84. end,
  85. score = 1.0,
  86. description = 'Text and HTML parts differ',
  87. group = 'body'
  88. }
  89. -- Date issues
  90. rspamd_config.MISSING_DATE = {
  91. callback = function(task)
  92. if rspamd_config:get_api_version() >= 5 then
  93. local date = task:get_header_raw('Date')
  94. if date == nil or date == '' then
  95. return true
  96. end
  97. end
  98. return false
  99. end,
  100. score = 1.0,
  101. description = 'Message date is missing',
  102. group = 'date'
  103. }
  104. rspamd_config.DATE_IN_FUTURE = {
  105. callback = function(task)
  106. if rspamd_config:get_api_version() >= 5 then
  107. local dm = task:get_date{format = 'message', gmt = true}
  108. local dt = task:get_date{format = 'connect', gmt = true}
  109. -- 2 hours
  110. if dm > 0 and dm - dt > 7200 then
  111. return true
  112. end
  113. end
  114. return false
  115. end,
  116. score = 4.0,
  117. description = 'Message date is in future',
  118. group = 'date'
  119. }
  120. rspamd_config.DATE_IN_PAST = {
  121. callback = function(task)
  122. if rspamd_config:get_api_version() >= 5 then
  123. local dm = task:get_date{format = 'message', gmt = true}
  124. local dt = task:get_date{format = 'connect', gmt = true}
  125. -- A day
  126. if dm > 0 and dt - dm > 86400 then
  127. return true
  128. end
  129. end
  130. return false
  131. end,
  132. score = 1.0,
  133. description = 'Message date is in past',
  134. group = 'date'
  135. }
  136. rspamd_config.R_SUSPICIOUS_URL = {
  137. callback = function(task)
  138. local urls = task:get_urls()
  139. if urls then
  140. for _,u in ipairs(urls) do
  141. if u:is_obscured() then
  142. task:insert_result('R_SUSPICIOUS_URL', 1.0, u:get_host())
  143. end
  144. end
  145. end
  146. return false
  147. end,
  148. score = 6.0,
  149. one_shot = true,
  150. description = 'Obfusicated or suspicious URL has been found in a message',
  151. group = 'url'
  152. }
  153. rspamd_config.BROKEN_HEADERS = {
  154. callback = function(task)
  155. return task:has_flag('broken_headers')
  156. end,
  157. score = 10.0,
  158. group = 'header',
  159. description = 'Headers structure is likely broken'
  160. }
  161. rspamd_config.HEADER_RCONFIRM_MISMATCH = {
  162. callback = function (task)
  163. local header_from = nil
  164. local cread = task:get_header('X-Confirm-Reading-To')
  165. if task:has_from('mime') then
  166. header_from = task:get_from('mime')[1]
  167. end
  168. local header_cread = nil
  169. if cread then
  170. local headers_cread = util.parse_mail_address(cread)
  171. if headers_cread then header_cread = headers_cread[1] end
  172. end
  173. if header_from and header_cread then
  174. if not string.find(header_from['addr'], header_cread['addr']) then
  175. return true
  176. end
  177. end
  178. return false
  179. end,
  180. score = 2.0,
  181. group = 'header',
  182. description = 'Read confirmation address is different to from address'
  183. }
  184. rspamd_config.HEADER_FORGED_MDN = {
  185. callback = function (task)
  186. local mdn = task:get_header('Disposition-Notification-To')
  187. if not mdn then return false end
  188. local header_rp = nil
  189. if task:has_from('smtp') then
  190. header_rp = task:get_from('smtp')[1]
  191. end
  192. -- Parse mail addr
  193. local headers_mdn = util.parse_mail_address(mdn)
  194. if headers_mdn and not header_rp then return true end
  195. if header_rp and not headers_mdn then return false end
  196. if not headers_mdn and not header_rp then return false end
  197. local found_match = false
  198. for _, h in ipairs(headers_mdn) do
  199. if util.strequal_caseless(h['addr'], header_rp['addr']) then
  200. found_match = true
  201. break
  202. end
  203. end
  204. return (not found_match)
  205. end,
  206. score = 2.0,
  207. group = 'header',
  208. description = 'Read confirmation address is different to return path'
  209. }
  210. local headers_unique = {
  211. 'Content-Type',
  212. 'Content-Transfer-Encoding',
  213. -- https://tools.ietf.org/html/rfc5322#section-3.6
  214. 'Date',
  215. 'From',
  216. 'Sender',
  217. 'Reply-To',
  218. 'To',
  219. 'Cc',
  220. 'Bcc',
  221. 'Message-ID',
  222. 'In-Reply-To',
  223. 'References',
  224. 'Subject'
  225. }
  226. rspamd_config.MULTIPLE_UNIQUE_HEADERS = {
  227. callback = function (task)
  228. local res = 0
  229. local res_tbl = {}
  230. for _,hdr in ipairs(headers_unique) do
  231. local h = task:get_header_full(hdr)
  232. if h and #h > 1 then
  233. res = res + 1
  234. table.insert(res_tbl, hdr)
  235. end
  236. end
  237. if res > 0 then
  238. return true,res,table.concat(res_tbl, ',')
  239. end
  240. return false
  241. end,
  242. score = 5.0,
  243. group = 'header',
  244. description = 'Repeated unique headers'
  245. }
  246. rspamd_config.ENVFROM_PRVS = {
  247. callback = function (task)
  248. --[[
  249. Detect PRVS/BATV addresses to avoid FORGED_SENDER
  250. https://en.wikipedia.org/wiki/Bounce_Address_Tag_Validation
  251. Signature syntax:
  252. prvs=TAG=USER@example.com BATV draft (https://tools.ietf.org/html/draft-levine-smtp-batv-01)
  253. prvs=USER=TAG@example.com
  254. btv1==TAG==USER@example.com Barracuda appliance
  255. msprvs1=TAG=USER@example.com Sparkpost email delivery service
  256. ]]--
  257. if not (task:has_from(1) and task:has_from(2)) then
  258. return false
  259. end
  260. local envfrom = task:get_from(1)
  261. local re_text = '^(?:(prvs|msprvs1)=([^=]+)=|btv1==[^=]+==)(.+@(.+))$'
  262. local re = rspamd_regexp.create_cached(re_text)
  263. local c = re:search(envfrom[1].addr:lower(), false, true)
  264. if not c then return false end
  265. local ef = c[1][4]
  266. -- See if it matches the From header
  267. local from = task:get_from(2)
  268. if ef == from[1].addr:lower() then
  269. return true
  270. end
  271. -- Check for prvs=USER=TAG@example.com
  272. local t = c[1][2]
  273. if t == 'prvs' then
  274. local efr = c[1][3] .. '@' .. c[1][5]
  275. if efr == from[1].addr:lower() then
  276. return true
  277. end
  278. end
  279. return false
  280. end,
  281. score = 0.0,
  282. description = "Envelope From is a PRVS address that matches the From address",
  283. group = 'prvs'
  284. }
  285. rspamd_config.ENVFROM_VERP = {
  286. callback = function (task)
  287. if not (task:has_from(1) and task:has_recipients(1)) then
  288. return false
  289. end
  290. local envfrom = task:get_from(1)
  291. local envrcpts = task:get_recipients(1)
  292. -- VERP only works for single recipient messages
  293. if #envrcpts > 1 then return false end
  294. -- Get recipient and compute VERP address
  295. local rcpt = envrcpts[1].addr:lower()
  296. local verp = rcpt:gsub('@','=')
  297. -- Get the user portion of the envfrom
  298. local ef_user = envfrom[1].user:lower()
  299. -- See if the VERP representation of the recipient appears in it
  300. if ef_user:find(verp, 1, true)
  301. and not ef_user:find('+caf_=' .. verp, 1, true) -- Google Forwarding
  302. and not ef_user:find('^srs[01]=') -- SRS
  303. then
  304. return true
  305. end
  306. return false
  307. end,
  308. score = 0.0,
  309. description = "Envelope From is a VERP address",
  310. group = "mailing_list"
  311. }
  312. rspamd_config.RCVD_TLS_ALL = {
  313. callback = function (task)
  314. local rcvds = task:get_header_full('Received')
  315. if not rcvds then return false end
  316. local count = 0
  317. local encrypted = 0
  318. for _, rcvd in ipairs(rcvds) do
  319. count = count + 1
  320. local r = rcvd['decoded']:lower()
  321. local with = r:match('%swith%s+(e?smtps?a?)')
  322. if with and with:match('esmtps') then
  323. encrypted = encrypted + 1
  324. end
  325. end
  326. if (count > 0 and count == encrypted) then
  327. return true
  328. end
  329. end,
  330. score = 0.0,
  331. description = "All hops used encrypted transports",
  332. group = "encryption"
  333. }
  334. rspamd_config.MISSING_FROM = {
  335. callback = function(task)
  336. local from = task:get_header('From')
  337. if from == nil or from == '' then
  338. return true
  339. end
  340. return false
  341. end,
  342. score = 2.0,
  343. group = 'header',
  344. description = 'Missing From: header'
  345. }
  346. rspamd_config.RCVD_HELO_USER = {
  347. callback = function (task)
  348. -- Check HELO argument from MTA
  349. local helo = task:get_helo()
  350. if (helo and helo:lower():find('^user$')) then
  351. return true
  352. end
  353. -- Check Received headers
  354. local rcvds = task:get_header_full('Received')
  355. if not rcvds then return false end
  356. for _, rcvd in ipairs(rcvds) do
  357. local r = rcvd['decoded']:lower()
  358. if (r:find("^%s*from%suser%s")) then return true end
  359. if (r:find("helo[%s=]user[%s%)]")) then return true end
  360. end
  361. end,
  362. description = 'HELO User spam pattern',
  363. score = 3.0
  364. }
  365. rspamd_config.URI_COUNT_ODD = {
  366. callback = function (task)
  367. local ct = task:get_header('Content-Type')
  368. if (ct and ct:lower():find('^multipart/alternative')) then
  369. local urls = task:get_urls()
  370. if (urls and (#urls % 2 == 1)) then
  371. return true
  372. end
  373. end
  374. end,
  375. description = 'Odd number of URIs in multipart/alternative message',
  376. score = 1.0
  377. }
  378. rspamd_config.HAS_ATTACHMENT = {
  379. callback = function (task)
  380. local parts = task:get_parts()
  381. if parts and #parts > 1 then
  382. for _, p in ipairs(parts) do
  383. local cd = p:get_header('Content-Disposition')
  384. if (cd and cd:lower():match('^attachment')) then
  385. return true
  386. end
  387. end
  388. end
  389. end,
  390. description = 'Message contains attachments'
  391. }
  392. rspamd_config.MV_CASE = {
  393. callback = function (task)
  394. local mv = task:get_header('Mime-Version', true)
  395. if (mv) then return true end
  396. end,
  397. description = 'Mime-Version .vs. MIME-Version',
  398. score = 0.5
  399. }
  400. rspamd_config.FAKE_REPLY = {
  401. callback = function (task)
  402. local subject = task:get_header('Subject')
  403. if (subject and subject:lower():find('^re:')) then
  404. local ref = task:get_header('References')
  405. local rt = task:get_header('In-Reply-To')
  406. if (not (ref or rt)) then return true end
  407. end
  408. return false
  409. end,
  410. description = 'Fake reply',
  411. score = 1.0
  412. }
  413. local check_from_id = rspamd_config:register_callback_symbol('CHECK_FROM', 1.0,
  414. function(task)
  415. local envfrom = task:get_from(1)
  416. local from = task:get_from(2)
  417. if (from and from[1] and not from[1].name) then
  418. task:insert_result('FROM_NO_DN', 1.0)
  419. elseif (from and from[1] and from[1].name and
  420. from[1].name:lower() == from[1].addr:lower()) then
  421. task:insert_result('FROM_DN_EQ_ADDR', 1.0)
  422. elseif (from and from[1] and from[1].name) then
  423. task:insert_result('FROM_HAS_DN', 1.0)
  424. -- Look for Mr/Mrs/Dr titles
  425. local n = from[1].name:lower()
  426. if (n:find('^mrs?[%.%s]') or n:find('^dr[%.%s]')) then
  427. task:insert_result('FROM_NAME_HAS_TITLE', 1.0)
  428. end
  429. end
  430. if (envfrom and from and envfrom[1] and from[1] and
  431. envfrom[1].addr:lower() == from[1].addr:lower())
  432. then
  433. task:insert_result('FROM_EQ_ENVFROM', 1.0)
  434. elseif (envfrom and envfrom[1] and envfrom[1].addr) then
  435. task:insert_result('FROM_NEQ_ENVFROM', 1.0, from and from[1].addr or '', envfrom[1].addr)
  436. end
  437. local to = task:get_recipients(2)
  438. if not (to and to[1] and #to == 1 and from) then return false end
  439. -- Check if FROM == TO
  440. if (to[1].addr:lower() == from[1].addr:lower()) then
  441. task:insert_result('TO_EQ_FROM', 1.0)
  442. elseif (to[1].domain and from[1].domain and
  443. to[1].domain:lower() == from[1].domain:lower()) then
  444. task:insert_result('TO_DOM_EQ_FROM_DOM', 1.0)
  445. end
  446. end
  447. )
  448. rspamd_config:register_virtual_symbol('FROM_NO_DN', 1.0, check_from_id)
  449. rspamd_config:set_metric_symbol('FROM_NO_DN', 0, 'From header does not have a display name')
  450. rspamd_config:register_virtual_symbol('FROM_DN_EQ_ADDR', 1.0, check_from_id)
  451. rspamd_config:set_metric_symbol('FROM_DN_EQ_ADDR', 1.0, 'From header display name is the same as the address')
  452. rspamd_config:register_virtual_symbol('FROM_HAS_DN', 1.0, check_from_id)
  453. rspamd_config:set_metric_symbol('FROM_HAS_DN', 0, 'From header has a display name')
  454. rspamd_config:register_virtual_symbol('FROM_NAME_HAS_TITLE', 1.0, check_from_id)
  455. rspamd_config:set_metric_symbol('FROM_NAME_HAS_TITLE', 1.0, 'From header display name has a title (Mr/Mrs/Dr)')
  456. rspamd_config:register_virtual_symbol('FROM_EQ_ENVFROM', 1.0, check_from_id)
  457. rspamd_config:set_metric_symbol('FROM_EQ_ENVFROM', 0, 'From address is the same as the envelope')
  458. rspamd_config:register_virtual_symbol('FROM_NEQ_ENVFROM', 1.0, check_from_id)
  459. rspamd_config:set_metric_symbol('FROM_NEQ_ENVFROM', 0, 'From address is different to the envelope')
  460. rspamd_config:register_virtual_symbol('TO_EQ_FROM', 1.0, check_from_id)
  461. rspamd_config:set_metric_symbol('TO_EQ_FROM', 0, 'To address matches the From address')
  462. rspamd_config:register_virtual_symbol('TO_DOM_EQ_FROM_DOM', 1.0, check_from_id)
  463. rspamd_config:set_metric_symbol('TO_DOM_EQ_FROM_DOM', 0, 'To domain is the same as the From domain')
  464. local check_to_cc_id = rspamd_config:register_callback_symbol('CHECK_TO_CC', 1.0,
  465. function(task)
  466. local rcpts = task:get_recipients(1)
  467. local to = task:get_recipients(2)
  468. local to_match_envrcpt = 0
  469. if (not to) then return false end
  470. -- Add symbol for recipient count
  471. if (#to > 50) then
  472. task:insert_result('RCPT_COUNT_GT_50', 1.0)
  473. else
  474. task:insert_result('RCPT_COUNT_' .. #to, 1.0)
  475. end
  476. -- Check for display names
  477. local to_dn_count = 0
  478. local to_dn_eq_addr_count = 0
  479. for _, toa in ipairs(to) do
  480. -- To: Recipients <noreply@dropbox.com>
  481. if (toa['name'] and (toa['name']:lower() == 'recipient'
  482. or toa['name']:lower() == 'recipients')) then
  483. task:insert_result('TO_DN_RECIPIENTS', 1.0)
  484. end
  485. if (toa['name'] and toa['name']:lower() == toa['addr']:lower()) then
  486. to_dn_eq_addr_count = to_dn_eq_addr_count + 1
  487. elseif (toa['name']) then
  488. to_dn_count = to_dn_count + 1
  489. end
  490. -- See if header recipients match envrcpts
  491. if (rcpts) then
  492. for _, rcpt in ipairs(rcpts) do
  493. if (toa and toa['addr'] and rcpt and rcpt['addr'] and
  494. rcpt['addr']:lower() == toa['addr']:lower())
  495. then
  496. to_match_envrcpt = to_match_envrcpt + 1
  497. end
  498. end
  499. end
  500. end
  501. if (to_dn_count == 0 and to_dn_eq_addr_count == 0) then
  502. task:insert_result('TO_DN_NONE', 1.0)
  503. elseif (to_dn_count == #to) then
  504. task:insert_result('TO_DN_ALL', 1.0)
  505. elseif (to_dn_count > 0) then
  506. task:insert_result('TO_DN_SOME', 1.0)
  507. end
  508. if (to_dn_eq_addr_count == #to) then
  509. task:insert_result('TO_DN_EQ_ADDR_ALL', 1.0)
  510. elseif (to_dn_eq_addr_count > 0) then
  511. task:insert_result('TO_DN_EQ_ADDR_SOME', 1.0)
  512. end
  513. -- See if header recipients match envelope recipients
  514. if (to_match_envrcpt == #to) then
  515. task:insert_result('TO_MATCH_ENVRCPT_ALL', 1.0)
  516. elseif (to_match_envrcpt > 0) then
  517. task:insert_result('TO_MATCH_ENVRCPT_SOME', 1.0)
  518. end
  519. end
  520. )
  521. rspamd_config:register_virtual_symbol('TO_DN_RECIPIENTS', 1.0, check_to_cc_id)
  522. rspamd_config:set_metric_symbol('TO_DN_RECIPIENTS', 2.0, 'To header display name is "Recipients"')
  523. rspamd_config:register_virtual_symbol('TO_DN_NONE', 1.0, check_to_cc_id)
  524. rspamd_config:set_metric_symbol('TO_DN_NONE', 0, 'None of the recipients have display names')
  525. rspamd_config:register_virtual_symbol('TO_DN_ALL', 1.0, check_to_cc_id)
  526. rspamd_config:set_metric_symbol('TO_DN_ALL', 0, 'All of the recipients have display names')
  527. rspamd_config:register_virtual_symbol('TO_DN_SOME', 1.0, check_to_cc_id)
  528. rspamd_config:set_metric_symbol('TO_DN_SOME', 0, 'Some of the recipients have display names')
  529. rspamd_config:register_virtual_symbol('TO_DN_EQ_ADDR_ALL', 1.0, check_to_cc_id)
  530. rspamd_config:set_metric_symbol('TO_DN_EQ_ADDR_ALL', 0, 'All of the recipients have display names that are the same as their address')
  531. rspamd_config:register_virtual_symbol('TO_DN_EQ_ADDR_SOME', 1.0, check_to_cc_id)
  532. rspamd_config:set_metric_symbol('TO_DN_EQ_ADDR_SOME', 0, 'Some of the recipients have display names that are the same as their address')
  533. rspamd_config:register_virtual_symbol('TO_MATCH_ENVRCPT_ALL', 1.0, check_to_cc_id)
  534. rspamd_config:set_metric_symbol('TO_MATCH_ENVRCPT_ALL', 0, 'All of the recipients match the envelope')
  535. rspamd_config:register_virtual_symbol('TO_MATCH_ENVRCPT_SOME', 1.0, check_to_cc_id)
  536. rspamd_config:set_metric_symbol('TO_MATCH_ENVRCPT_SOME', 0, 'Some of the recipients match the envelope')
  537. rspamd_config.CHECK_RECEIVED = {
  538. callback = function (task)
  539. local received = task:get_received_headers()
  540. task:insert_result('RCVD_COUNT_' .. #received, 1.0)
  541. end
  542. }
  543. rspamd_config.HAS_X_PRIO = {
  544. callback = function (task)
  545. local xprio = task:get_header('X-Priority');
  546. if not xprio then return false end
  547. local _,_,x = xprio:find('^%s?(%d+)');
  548. if (x) then
  549. task:insert_result('HAS_X_PRIO_' .. x, 1.0)
  550. end
  551. end
  552. }
  553. local check_replyto_id = rspamd_config:register_callback_symbol('CHECK_REPLYTO', 1.0,
  554. function (task)
  555. local replyto = task:get_header('Reply-To')
  556. if not replyto then return false end
  557. local rt = util.parse_mail_address(replyto)
  558. if not (rt and rt[1]) then
  559. task:insert_result('REPLYTO_UNPARSEABLE', 1.0)
  560. return false
  561. else
  562. task:insert_result('HAS_REPLYTO', 1.0)
  563. end
  564. -- See if Reply-To matches From in some way
  565. local from = task:get_from(2)
  566. local from_h = task:get_header('From')
  567. if not (from and from[1]) then return false end
  568. if (from_h and from_h == replyto) then
  569. -- From and Reply-To are identical
  570. task:insert_result('REPLYTO_EQ_FROM', 1.0)
  571. else
  572. if (from and from[1]) then
  573. -- See if From and Reply-To addresses match
  574. if (from[1].addr:lower() == rt[1].addr:lower()) then
  575. task:insert_result('REPLYTO_ADDR_EQ_FROM', 1.0)
  576. elseif from[1].domain and rt[1].domain then
  577. if (from[1].domain:lower() == rt[1].domain:lower()) then
  578. task:insert_result('REPLYTO_DOM_EQ_FROM_DOM', 1.0)
  579. else
  580. task:insert_result('REPLYTO_DOM_NEQ_FROM_DOM', 1.0)
  581. end
  582. end
  583. -- See if the Display Names match
  584. if (from[1].name and rt[1].name and from[1].name:lower() == rt[1].name:lower()) then
  585. task:insert_result('REPLYTO_DN_EQ_FROM_DN', 1.0)
  586. end
  587. end
  588. end
  589. end
  590. )
  591. rspamd_config:register_virtual_symbol('REPLYTO_UNPARSEABLE', 1.0, check_replyto_id)
  592. rspamd_config:set_metric_symbol('REPLYTO_UNPARSEABLE', 1.0, 'Reply-To header could not be parsed')
  593. rspamd_config:register_virtual_symbol('HAS_REPLYTO', 1.0, check_replyto_id)
  594. rspamd_config:set_metric_symbol('HAS_REPLYTO', 0, 'Has Reply-To header')
  595. rspamd_config:register_virtual_symbol('REPLYTO_EQ_FROM', 1.0, check_replyto_id)
  596. rspamd_config:set_metric_symbol('REPLYTO_EQ_FROM', 0, 'Reply-To header is identical to From header')
  597. rspamd_config:register_virtual_symbol('REPLYTO_ADDR_EQ_FROM', 1.0, check_replyto_id)
  598. rspamd_config:set_metric_symbol('REPLYTO_ADDR_EQ_FROM', 0, 'Reply-To address is the same as From')
  599. rspamd_config:register_virtual_symbol('REPLYTO_DOM_EQ_FROM_DOM', 1.0, check_replyto_id)
  600. rspamd_config:set_metric_symbol('REPLYTO_DOM_EQ_FROM_DOM', 0, 'Reply-To domain matches the From domain')
  601. rspamd_config:register_virtual_symbol('REPLYTO_DOM_NEQ_FROM_DOM', 1.0, check_replyto_id)
  602. rspamd_config:set_metric_symbol('REPLYTO_DOM_NEQ_FROM_DOM', 0, 'Reply-To domain does not match the From domain')
  603. rspamd_config:register_virtual_symbol('REPLYTO_DN_EQ_FROM_DN', 1.0, check_replyto_id)
  604. rspamd_config:set_metric_symbol('REPLYTO_DN_EQ_FROM_DN', 0, 'Reply-To display name matches From')
  605. local check_mime_id = rspamd_config:register_callback_symbol('CHECK_MIME', 1.0,
  606. function (task)
  607. local parts = task:get_parts()
  608. if not parts then return false end
  609. -- Make sure there is a MIME-Version header
  610. local mv = task:get_header('MIME-Version')
  611. if (not mv) then
  612. task:insert_result('MISSING_MIME_VERSION', 1.0)
  613. end
  614. local found_ma = false
  615. local found_plain = false
  616. local found_html = false
  617. for _,p in ipairs(parts) do
  618. local mtype,subtype = p:get_type()
  619. local ctype = mtype:lower() .. '/' .. subtype:lower()
  620. if (ctype == 'multipart/alternative') then
  621. found_ma = true
  622. end
  623. if (ctype == 'text/plain') then
  624. found_plain = true
  625. end
  626. if (ctype == 'text/html') then
  627. found_html = true
  628. end
  629. end
  630. if (found_ma) then
  631. if (not found_plain) then
  632. task:insert_result('MIME_MA_MISSING_TEXT', 1.0)
  633. end
  634. if (not found_html) then
  635. task:insert_result('MIME_MA_MISSING_HTML', 1.0)
  636. end
  637. end
  638. end
  639. )
  640. rspamd_config:register_virtual_symbol('MISSING_MIME_VERSION', 1.0, check_mime_id)
  641. rspamd_config:set_metric_symbol('MISSING_MIME_VERSION', 2.0, 'MIME-Version header is missing')
  642. rspamd_config:register_virtual_symbol('MIME_MA_MISSING_TEXT', 1.0, check_mime_id)
  643. rspamd_config:set_metric_symbol('MIME_MA_MISSING_TEXT', 2.0, 'MIME multipart/alternative missing text/plain part')
  644. rspamd_config:register_virtual_symbol('MIME_MA_MISSING_HTML', 1.0, check_mime_id)
  645. rspamd_config:set_metric_symbol('MIME_MA_MISSING_HTML', 1.0, 'multipart/alternative missing text/html part')
  646. -- Used to be called IS_LIST
  647. rspamd_config.PREVIOUSLY_DELIVERED = {
  648. callback = function(task)
  649. if not task:has_recipients(2) then return false end
  650. local to = task:get_recipients(2)
  651. local rcvds = task:get_header_full('Received')
  652. if not rcvds then return false end
  653. for _, rcvd in ipairs(rcvds) do
  654. local _,_,addr = rcvd['decoded']:lower():find("%sfor%s<(.-)>")
  655. if addr then
  656. for _, toa in ipairs(to) do
  657. if toa and toa.addr:lower() == addr then
  658. return true, addr
  659. end
  660. end
  661. return false
  662. end
  663. end
  664. end,
  665. description = 'Message either to a list or was forwarded',
  666. score = 0.0
  667. }
  668. -- Requires freemail maps loaded in multimap
  669. local function freemail_reply_neq_from(task)
  670. local frt = task:get_symbol('FREEMAIL_REPLYTO')
  671. local ff = task:get_symbol('FREEMAIL_FROM')
  672. if (frt and ff and frt['options'] and ff['options'] and
  673. frt['options'][1] ~= ff['options'][1])
  674. then
  675. return true
  676. end
  677. return false
  678. end
  679. local freemail_reply_neq_from_id = rspamd_config:register_symbol({
  680. name = 'FREEMAIL_REPLYTO_NEQ_FROM_DOM',
  681. callback = freemail_reply_neq_from,
  682. description = 'Freemail From and Reply-To, but to different Freemail services',
  683. score = 3.0
  684. })
  685. rspamd_config:register_dependency(freemail_reply_neq_from_id, 'FREEMAIL_REPLYTO')
  686. rspamd_config:register_dependency(freemail_reply_neq_from_id, 'FREEMAIL_FROM')
  687. rspamd_config.OMOGRAPH_URL = {
  688. callback = function(task)
  689. local urls = task:get_urls()
  690. if urls then
  691. for _,u in ipairs(urls) do
  692. local h = u:get_host()
  693. if h then
  694. local non_latin,total = util.count_non_ascii(h)
  695. if non_latin ~= total and non_latin > 0 then
  696. return true, 1.0, h
  697. end
  698. end
  699. end
  700. end
  701. return false
  702. end,
  703. score = 5.0,
  704. description = 'Url contains both latin and non-latin characters'
  705. }