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

headers.lua 40KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006
  1. -- Actually these regular expressions were obtained from SpamAssassin project, so they are licensed by apache license:
  2. --
  3. -- Licensed to the Apache Software Foundation (ASF) under one or more
  4. -- contributor license agreements. See the NOTICE file distributed with
  5. -- this work for additional information regarding copyright ownership.
  6. -- The ASF licenses this file to you under the Apache License, Version 2.0
  7. -- (the "License"); you may not use this file except in compliance with
  8. -- the License. You may obtain a copy of the License at:
  9. --
  10. -- http://www.apache.org/licenses/LICENSE-2.0
  11. --
  12. -- Unless required by applicable law or agreed to in writing, software
  13. -- distributed under the License is distributed on an "AS IS" BASIS,
  14. -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. -- See the License for the specific language governing permissions and
  16. -- limitations under the License.
  17. --
  18. -- Definitions of header regexps
  19. local reconf = config['regexp']
  20. local rspamd_regexp = require "rspamd_regexp"
  21. -- Subject needs encoding
  22. -- Define encodings types
  23. local subject_encoded_b64 = 'Subject=/=\\?\\S+\\?B\\?/iX'
  24. local subject_encoded_qp = 'Subject=/=\\?\\S+\\?Q\\?/iX'
  25. -- Define whether subject must be encoded (contains non-7bit characters)
  26. local subject_needs_mime = 'Subject=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/X'
  27. -- Final rule
  28. reconf['SUBJECT_NEEDS_ENCODING'] = {
  29. re = string.format('!(%s) & !(%s) & (%s)', subject_encoded_b64, subject_encoded_qp, subject_needs_mime),
  30. score = 1.0,
  31. mime_only = true,
  32. description = 'Subject needs encoding',
  33. group = 'headers'
  34. }
  35. local from_encoded_b64 = 'From=/=\\?\\S+\\?B\\?/iX'
  36. local from_encoded_qp = 'From=/=\\?\\S+\\?Q\\?/iX'
  37. local raw_from_needs_mime = 'From=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/X'
  38. reconf['FROM_NEEDS_ENCODING'] = {
  39. re = string.format('!(%s) & !(%s) & (%s)', from_encoded_b64, from_encoded_qp, raw_from_needs_mime),
  40. score = 1.0,
  41. mime_only = true,
  42. description = 'From header needs encoding',
  43. group = 'headers'
  44. }
  45. local to_encoded_b64 = 'To=/=\\?\\S+\\?B\\?/iX'
  46. local to_encoded_qp = 'To=/=\\?\\S+\\?Q\\?/iX'
  47. local raw_to_needs_mime = 'To=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/X'
  48. reconf['TO_NEEDS_ENCODING'] = {
  49. re = string.format('!(%s) & !(%s) & (%s)', to_encoded_b64, to_encoded_qp, raw_to_needs_mime),
  50. score = 1.0,
  51. mime_only = true,
  52. description = 'To header needs encoding',
  53. group = 'headers'
  54. }
  55. -- Detects that there is no space in From header (e.g. Some Name<some@host>)
  56. reconf['R_NO_SPACE_IN_FROM'] = {
  57. re = 'From=/\\S<[-\\w\\.]+\\@[-\\w\\.]+>/X',
  58. score = 1.0,
  59. mime_only = true,
  60. description = 'No space in from header',
  61. group = 'headers'
  62. }
  63. reconf['TO_WRAPPED_IN_SPACES'] = {
  64. re = [[To=/<\s[-.\w]+\@[-.\w]+\s>/X]],
  65. score = 2.0,
  66. mime_only = true,
  67. description = 'To address is wrapped in spaces inside angle brackets (e.g. display-name < local-part@domain >)',
  68. group = 'headers'
  69. }
  70. -- Detects missing Subject header
  71. reconf['MISSING_SUBJECT'] = {
  72. re = '!raw_header_exists(Subject)',
  73. score = 2.0,
  74. mime_only = true,
  75. description = 'Subject header is missing',
  76. group = 'headers'
  77. }
  78. rspamd_config.EMPTY_SUBJECT = {
  79. score = 1.0,
  80. mime_only = true,
  81. description = 'Subject header is empty',
  82. group = 'headers',
  83. callback = function(task)
  84. local hdr = task:get_header('Subject')
  85. if hdr and #hdr == 0 then
  86. return true
  87. end
  88. return false
  89. end
  90. }
  91. -- Detects missing To header
  92. reconf['MISSING_TO'] = {
  93. re = '!raw_header_exists(To)',
  94. score = 2.0,
  95. description = 'To header is missing',
  96. group = 'headers',
  97. mime_only = true,
  98. }
  99. -- Detects undisclosed recipients
  100. local undisc_rcpt = 'To=/^<?undisclosed[- ]recipient/Hi'
  101. reconf['R_UNDISC_RCPT'] = {
  102. re = string.format('(%s)', undisc_rcpt),
  103. score = 3.0,
  104. description = 'Recipients are absent or undisclosed',
  105. group = 'headers',
  106. mime_only = true,
  107. }
  108. -- Detects missing Message-Id
  109. local has_mid = 'header_exists(Message-Id)'
  110. reconf['MISSING_MID'] = {
  111. re = '!header_exists(Message-Id)',
  112. score = 2.5,
  113. description = 'Message id is missing',
  114. group = 'headers',
  115. mime_only = true,
  116. }
  117. -- Received seems to be fake
  118. reconf['R_RCVD_SPAMBOTS'] = {
  119. re = 'Received=/^from \\[\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\] by [-.\\w+]{5,255}; [SMTWF][a-z][a-z],' ..
  120. ' [\\s\\d]?\\d [JFMAJSOND][a-z][a-z] \\d{4} \\d{2}:\\d{2}:\\d{2} [-+]\\d{4}$/mH',
  121. score = 3.0,
  122. description = 'Spambots signatures in received headers',
  123. group = 'headers',
  124. mime_only = true,
  125. }
  126. -- Charset is missing in message
  127. reconf['R_MISSING_CHARSET'] = {
  128. re = string.format('!is_empty_body() & content_type_is_type(text) & content_type_is_subtype(plain) & !content_type_has_param(charset) & !%s',
  129. 'compare_transfer_encoding(7bit)'),
  130. score = 2.5,
  131. description = 'Charset is missing in a message',
  132. group = 'headers',
  133. mime_only = true,
  134. }
  135. -- Subject seems to be spam
  136. reconf['R_SAJDING'] = {
  137. re = 'Subject=/\\bsajding(?:om|a)?\\b/iH',
  138. score = 8.0,
  139. description = 'Subject seems to be spam',
  140. group = 'headers',
  141. mime_only = true,
  142. }
  143. -- Find forged Outlook MUA
  144. -- Yahoo groups messages
  145. local yahoo_bulk = 'Received=/from \\[\\S+\\] by \\S+\\.(?:groups|scd|dcn)\\.yahoo\\.com with NNFMP/H'
  146. -- Outlook MUA
  147. local outlook_mua = 'X-Mailer=/^Microsoft Outlook\\b/H'
  148. local any_outlook_mua = 'X-Mailer=/^Microsoft Outlook\\b/H'
  149. reconf['FORGED_OUTLOOK_HTML'] = {
  150. re = string.format('!%s & %s & %s', yahoo_bulk, outlook_mua, 'has_only_html_part()'),
  151. score = 5.0,
  152. description = 'Forged outlook HTML signature',
  153. group = 'headers',
  154. mime_only = true,
  155. }
  156. -- Recipients seems to be likely with each other (only works when recipients count is more than 5 recipients)
  157. reconf['SUSPICIOUS_RECIPS'] = {
  158. re = 'compare_recipients_distance(0.65)',
  159. score = 1.5,
  160. description = 'Recipients seems to be autogenerated (works if recipients count is more than 5)',
  161. group = 'headers',
  162. mime_only = true,
  163. }
  164. -- Recipients list seems to be sorted
  165. reconf['SORTED_RECIPS'] = {
  166. re = 'is_recipients_sorted()',
  167. score = 3.5,
  168. description = 'Recipients list seems to be sorted',
  169. group = 'headers',
  170. mime_only = true,
  171. }
  172. -- Spam string at the end of message to make statistics faults
  173. reconf['TRACKER_ID'] = {
  174. re = '/^[a-z0-9]{6,24}[-_a-z0-9]{12,36}[a-z0-9]{6,24}\\s*\\z/isPr',
  175. score = 3.84,
  176. description = 'Spam string at the end of message to make statistics fault',
  177. group = 'headers',
  178. mime_only = true,
  179. }
  180. -- From contains only 7bit characters (parsed headers are used)
  181. local from_needs_mime = 'From=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/Hr'
  182. -- From that contains encoded characters while base 64 is not needed as all symbols are 7bit
  183. reconf['FROM_EXCESS_BASE64'] = {
  184. re = string.format('%s & !%s', from_encoded_b64, from_needs_mime),
  185. score = 1.5,
  186. description = 'From that contains encoded characters while base 64 is not needed as all symbols are 7bit',
  187. group = 'excessb64',
  188. mime_only = true,
  189. }
  190. -- From that contains encoded characters while quoted-printable is not needed as all symbols are 7bit
  191. reconf['FROM_EXCESS_QP'] = {
  192. re = string.format('%s & !%s', from_encoded_qp, from_needs_mime),
  193. score = 1.2,
  194. description = 'From that contains encoded characters while quoted-printable is not needed as all symbols are 7bit',
  195. group = 'excessqp'
  196. }
  197. -- To contains only 7bit characters (parsed headers are used)
  198. local to_needs_mime = 'To=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/Hr'
  199. -- To that contains encoded characters while base 64 is not needed as all symbols are 7bit
  200. reconf['TO_EXCESS_BASE64'] = {
  201. re = string.format('%s & !%s', to_encoded_b64, to_needs_mime),
  202. score = 1.5,
  203. description = 'To that contains encoded characters while base 64 is not needed as all symbols are 7bit',
  204. group = 'excessb64'
  205. }
  206. -- To that contains encoded characters while quoted-printable is not needed as all symbols are 7bit
  207. -- Final rule
  208. reconf['TO_EXCESS_QP'] = {
  209. re = string.format('%s & !%s', to_encoded_qp, to_needs_mime),
  210. score = 1.2,
  211. description = 'To that contains encoded characters while quoted-printable is not needed as all symbols are 7bit',
  212. group = 'excessqp'
  213. }
  214. -- Reply-To that contains encoded characters while base 64 is not needed as all symbols are 7bit
  215. -- Regexp that checks that Reply-To header is encoded with base64 (search in raw headers)
  216. local replyto_encoded_b64 = 'Reply-To=/\\=\\?\\S+\\?B\\?/iX'
  217. -- Reply-To contains only 7bit characters (parsed headers are used)
  218. local replyto_needs_mime = 'Reply-To=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/Hr'
  219. -- Final rule
  220. reconf['REPLYTO_EXCESS_BASE64'] = {
  221. re = string.format('%s & !%s', replyto_encoded_b64, replyto_needs_mime),
  222. score = 1.5,
  223. description = 'Reply-To that contains encoded characters while base 64 is not needed as all symbols are 7bit',
  224. group = 'excessb64'
  225. }
  226. -- Reply-To that contains encoded characters while quoted-printable is not needed as all symbols are 7bit
  227. -- Regexp that checks that Reply-To header is encoded with quoted-printable (search in raw headers)
  228. local replyto_encoded_qp = 'Reply-To=/\\=\\?\\S+\\?Q\\?/iX'
  229. -- Final rule
  230. reconf['REPLYTO_EXCESS_QP'] = {
  231. re = string.format('%s & !%s', replyto_encoded_qp, replyto_needs_mime),
  232. score = 1.2,
  233. description = 'Reply-To that contains encoded characters while quoted-printable is not needed as all symbols are 7bit',
  234. group = 'excessqp'
  235. }
  236. -- Cc that contains encoded characters while base 64 is not needed as all symbols are 7bit
  237. -- Regexp that checks that Cc header is encoded with base64 (search in raw headers)
  238. local cc_encoded_b64 = 'Cc=/\\=\\?\\S+\\?B\\?/iX'
  239. -- Co contains only 7bit characters (parsed headers are used)
  240. local cc_needs_mime = 'Cc=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/Hr'
  241. -- Final rule
  242. reconf['CC_EXCESS_BASE64'] = {
  243. re = string.format('%s & !%s', cc_encoded_b64, cc_needs_mime),
  244. score = 1.5,
  245. description = 'Cc that contains encoded characters while base 64 is not needed as all symbols are 7bit',
  246. group = 'excessb64'
  247. }
  248. -- Cc that contains encoded characters while quoted-printable is not needed as all symbols are 7bit
  249. -- Regexp that checks that Cc header is encoded with quoted-printable (search in raw headers)
  250. local cc_encoded_qp = 'Cc=/\\=\\?\\S+\\?Q\\?/iX'
  251. -- Final rule
  252. reconf['CC_EXCESS_QP'] = {
  253. re = string.format('%s & !%s', cc_encoded_qp, cc_needs_mime),
  254. score = 1.2,
  255. description = 'Cc that contains encoded characters while quoted-printable is not needed as all symbols are 7bit',
  256. group = 'excessqp'
  257. }
  258. local subj_encoded_b64 = 'Subject=/\\=\\?\\S+\\?B\\?/iX'
  259. local subj_needs_mime = 'Subject=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/Hr'
  260. reconf['SUBJ_EXCESS_BASE64'] = {
  261. re = string.format('%s & !%s', subj_encoded_b64, subj_needs_mime),
  262. score = 1.5,
  263. description = 'Subject is unnecessarily encoded in base64',
  264. group = 'excessb64'
  265. }
  266. local subj_encoded_qp = 'Subject=/\\=\\?\\S+\\?Q\\?/iX'
  267. reconf['SUBJ_EXCESS_QP'] = {
  268. re = string.format('%s & !%s', subj_encoded_qp, subj_needs_mime),
  269. score = 1.2,
  270. description = 'Subject is unnecessarily encoded in quoted-printable',
  271. group = 'excessqp'
  272. }
  273. -- Detect forged outlook headers
  274. -- OE X-Mailer header
  275. local oe_mua = 'X-Mailer=/\\bOutlook Express [456]\\./H'
  276. -- OE Message ID format
  277. local oe_msgid_1 = 'Message-Id=/^<?[A-Za-z0-9-]{7}[A-Za-z0-9]{20}\\@hotmail\\.com>?$/mH'
  278. local oe_msgid_2 = 'Message-Id=/^<?(?:[0-9a-f]{8}|[0-9a-f]{12})\\$[0-9a-f]{8}\\$[0-9a-f]{8}\\@\\S+>?$/H'
  279. -- EZLM remail of message
  280. local lyris_ezml_remailer = 'List-Unsubscribe=/<mailto:(?:leave-\\S+|\\S+-unsubscribe)\\@\\S+>$/H'
  281. -- Header of wacky sendmail
  282. local wacky_sendmail_version = 'Received=/\\/CWT\\/DCE\\)/H'
  283. -- Iplanet received header
  284. local iplanet_messaging_server = 'Received=/iPlanet Messaging Server/H'
  285. -- Hotmail message id
  286. local hotmail_baydav_msgid = 'Message-Id=/^<?BAY\\d+-DAV\\d+[A-Z0-9]{25}\\@phx\\.gbl?>$/H'
  287. -- Sympatico message id
  288. local sympatico_msgid = 'Message-Id=/^<?BAYC\\d+-PASMTP\\d+[A-Z0-9]{25}\\@CEZ\\.ICE>?$/H'
  289. -- Mailman message id
  290. local mailman_msgid = 'Message-ID=/^<mailman\\.\\d+\\.\\d+\\.\\d+\\..+\\@\\S+>$/H'
  291. -- Message id seems to be forged
  292. local unusable_msgid = string.format('(%s | %s | %s | %s | %s | %s)',
  293. lyris_ezml_remailer, wacky_sendmail_version, iplanet_messaging_server, hotmail_baydav_msgid, sympatico_msgid, mailman_msgid)
  294. -- Outlook express data seems to be forged
  295. local forged_oe = string.format('(%s & !%s & !%s & !%s)', oe_mua, oe_msgid_1, oe_msgid_2, unusable_msgid)
  296. -- Outlook specific headers
  297. local outlook_dollars_mua = 'X-Mailer=/^Microsoft Outlook(?: 8| CWS, Build 9|, Build 10)\\./H'
  298. local outlook_dollars_other = 'Message-Id=/^<?\\!\\~\\!>?/H'
  299. local vista_msgid = 'Message-Id=/^<?[A-F\\d]{32}\\@\\S+>?$/H'
  300. local ims_msgid = 'Message-Id=/^<?[A-F\\d]{36,40}\\@\\S+>?$/H'
  301. -- Forged outlook headers
  302. local forged_outlook_dollars = string.format('(%s & !%s & !%s & !%s & !%s & !%s)',
  303. outlook_dollars_mua, oe_msgid_2, outlook_dollars_other, vista_msgid, ims_msgid, unusable_msgid)
  304. -- Outlook versions that should be excluded from summary rule
  305. local fmo_excl_o3416 = 'X-Mailer=/^Microsoft Outlook, Build 10.0.3416$/H'
  306. local fmo_excl_oe3790 = 'X-Mailer=/^Microsoft Outlook Express 6.00.3790.3959$/H'
  307. -- Summary rule for forged outlook
  308. reconf['FORGED_MUA_OUTLOOK'] = {
  309. re = string.format('(%s | %s) & !%s & !%s & !%s',
  310. forged_oe, forged_outlook_dollars, fmo_excl_o3416, fmo_excl_oe3790, vista_msgid),
  311. score = 3.0,
  312. description = 'Forged outlook MUA',
  313. group = 'mua'
  314. }
  315. -- HTML outlook signs
  316. local mime_html = 'content_type_is_type(text) & content_type_is_subtype(/.?html/)'
  317. local tag_exists_html = 'has_html_tag(html)'
  318. local tag_exists_head = 'has_html_tag(head)'
  319. local tag_exists_meta = 'has_html_tag(meta)'
  320. local tag_exists_body = 'has_html_tag(body)'
  321. reconf['FORGED_OUTLOOK_TAGS'] = {
  322. re = string.format('!%s & %s & %s & !(%s & %s & %s & %s)',
  323. yahoo_bulk, any_outlook_mua, mime_html, tag_exists_html, tag_exists_head,
  324. tag_exists_meta, tag_exists_body),
  325. score = 2.1,
  326. description = "Message pretends to be send from Outlook but has 'strange' tags",
  327. group = 'headers'
  328. }
  329. -- Forged OE/MSO boundary
  330. reconf['SUSPICIOUS_BOUNDARY'] = {
  331. re = 'Content-Type=/^\\s*multipart.+boundary="----=_NextPart_000_[A-Z\\d]{4}_(00EBFFA4|0102FFA4|32C6FFA4|3302FFA4)\\.[A-Z\\d]{8}"[\\r\\n]*$/siX',
  332. score = 5.0,
  333. description = 'Suspicious boundary in header Content-Type',
  334. group = 'mua'
  335. }
  336. -- Forged OE/MSO boundary
  337. reconf['SUSPICIOUS_BOUNDARY2'] = {
  338. re = 'Content-Type=/^\\s*multipart.+boundary="----=_NextPart_000_[A-Z\\d]{4}_(01C6527E)\\.[A-Z\\d]{8}"[\\r\\n]*$/siX',
  339. score = 4.0,
  340. description = 'Suspicious boundary in header Content-Type',
  341. group = 'mua'
  342. }
  343. -- Forged OE/MSO boundary
  344. reconf['SUSPICIOUS_BOUNDARY3'] = {
  345. re = 'Content-Type=/^\\s*multipart.+boundary="-----000-00\\d\\d-01C[\\dA-F]{5}-[\\dA-F]{8}"[\\r\\n]*$/siX',
  346. score = 3.0,
  347. description = 'Suspicious boundary in header Content-Type',
  348. group = 'mua'
  349. }
  350. -- Forged OE/MSO boundary
  351. local suspicious_boundary_01C4 = 'Content-Type=/^\\s*multipart.+boundary="----=_NextPart_000_[A-Z\\d]{4}_01C4[\\dA-F]{4}\\.[A-Z\\d]{8}"[\\r\\n]*$/siX'
  352. local suspicious_boundary_01C4_date = 'Date=/^\\s*\\w\\w\\w,\\s+\\d+\\s+\\w\\w\\w 20(0[56789]|1\\d)/'
  353. reconf['SUSPICIOUS_BOUNDARY4'] = {
  354. re = string.format('(%s) & (%s)', suspicious_boundary_01C4, suspicious_boundary_01C4_date),
  355. score = 4.0,
  356. description = 'Suspicious boundary in header Content-Type',
  357. group = 'mua'
  358. }
  359. -- Detect forged The Bat! headers
  360. -- The Bat! X-Mailer header
  361. local thebat_mua_any = 'X-Mailer=/^\\s*The Bat!/H'
  362. -- The Bat! common Message-ID template
  363. local thebat_msgid_common = 'Message-ID=/^<?\\d+\\.\\d+\\@\\S+>?$/mH'
  364. -- Correct The Bat! Message-ID template
  365. local thebat_msgid = 'Message-ID=/^<?\\d+\\.(19[789]\\d|20\\d\\d)(0\\d|1[012])([012]\\d|3[01])([0-5]\\d)([0-5]\\d)([0-5]\\d)\\@\\S+>?/mH'
  366. -- Summary rule for forged The Bat! Message-ID header
  367. reconf['FORGED_MUA_THEBAT_MSGID'] = {
  368. re = string.format('(%s) & !(%s) & (%s) & !(%s)', thebat_mua_any, thebat_msgid, thebat_msgid_common, unusable_msgid),
  369. score = 4.0,
  370. description = 'Message pretends to be send from The Bat! but has forged Message-ID',
  371. group = 'mua'
  372. }
  373. -- Summary rule for forged The Bat! Message-ID header with unknown template
  374. reconf['FORGED_MUA_THEBAT_MSGID_UNKNOWN'] = {
  375. re = string.format('(%s) & !(%s) & !(%s) & !(%s)', thebat_mua_any, thebat_msgid, thebat_msgid_common, unusable_msgid),
  376. score = 3.0,
  377. description = 'Message pretends to be send from The Bat! but has forged Message-ID',
  378. group = 'mua'
  379. }
  380. -- Detect forged KMail headers
  381. -- KMail User-Agent header
  382. local kmail_mua = 'User-Agent=/^\\s*KMail\\/1\\.\\d+\\.\\d+/H'
  383. -- KMail common Message-ID template
  384. local kmail_msgid_common = 'Message-Id=/^<?\\s*\\d+\\.\\d+\\.\\S+\\@\\S+>?$/mH'
  385. function kmail_msgid (task)
  386. local regexp_text = '<(\\S+)>\\|(19[789]\\d|20\\d\\d)(0\\d|1[012])([012]\\d|3[01])([0-5]\\d)([0-5]\\d)\\.\\d+\\.\\1$'
  387. local re = rspamd_regexp.create_cached(regexp_text)
  388. local header_msgid = task:get_header('Message-Id')
  389. if header_msgid then
  390. local header_from = task:get_header('From')
  391. if header_from and re:match(header_from.."|"..header_msgid) then return true end
  392. end
  393. return false
  394. end
  395. -- Summary rule for forged KMail Message-ID header
  396. reconf['FORGED_MUA_KMAIL_MSGID'] = {
  397. re = string.format('(%s) & (%s) & !(%s) & !(%s)', kmail_mua, kmail_msgid_common, 'kmail_msgid', unusable_msgid),
  398. score = 3.0,
  399. description = 'Message pretends to be send from KMail but has forged Message-ID',
  400. group = 'mua'
  401. }
  402. -- Summary rule for forged KMail Message-ID header with unknown template
  403. reconf['FORGED_MUA_KMAIL_MSGID_UNKNOWN'] = {
  404. re = string.format('(%s) & !(%s) & !(%s)', kmail_mua, kmail_msgid_common, unusable_msgid),
  405. score = 2.5,
  406. description = 'Message pretends to be send from KMail but has forged Message-ID',
  407. group = 'mua'
  408. }
  409. -- Detect forged Opera Mail headers
  410. -- Opera Mail User-Agent header
  411. local opera1x_mua = 'User-Agent=/^\\s*Opera Mail\\/1[01]\\.\\d+ /H'
  412. -- Opera Mail Message-ID template
  413. local opera1x_msgid = 'Message-ID=/^<?op\\.[a-z\\d]{14}\\@\\S+>?$/H'
  414. -- Suspicious Opera Mail User-Agent header
  415. local suspicious_opera10w_mua = 'User-Agent=/^\\s*Opera Mail\\/10\\.\\d+ \\(Windows\\)$/H'
  416. -- Suspicious Opera Mail Message-ID, apparently from KMail
  417. local suspicious_opera10w_msgid = 'Message-Id=/^<?2009\\d{8}\\.\\d+\\.\\S+\\@\\S+?>$/H'
  418. -- Summary rule for forged Opera Mail User-Agent header and Message-ID header from KMail
  419. reconf['SUSPICIOUS_OPERA_10W_MSGID'] = {
  420. re = string.format('(%s) & (%s)', suspicious_opera10w_mua, suspicious_opera10w_msgid),
  421. score = 4.0,
  422. description = 'Message pretends to be send from suspicious Opera Mail/10.x (Windows) but has forged Message-ID, apparently from KMail',
  423. group = 'mua'
  424. }
  425. -- Summary rule for forged Opera Mail Message-ID header
  426. reconf['FORGED_MUA_OPERA_MSGID'] = {
  427. re = string.format('(%s) & !(%s) & !(%s) & !(%s)', opera1x_mua, opera1x_msgid, reconf['SUSPICIOUS_OPERA_10W_MSGID']['re'], unusable_msgid),
  428. score = 4.0,
  429. description = 'Message pretends to be send from Opera Mail but has forged Message-ID',
  430. group = 'mua'
  431. }
  432. -- Detect forged Mozilla Mail/Thunderbird/Seamonkey/Postbox headers
  433. -- Mozilla based X-Mailer
  434. local user_agent_mozilla5 = 'User-Agent=/^\\s*Mozilla\\/5\\.0/H'
  435. local user_agent_thunderbird = 'User-Agent=/^\\s*(Thunderbird|Mozilla Thunderbird|Mozilla\\/.*Gecko\\/.*(Thunderbird|Icedove)\\/)/H'
  436. local user_agent_seamonkey = 'User-Agent=/^\\s*Mozilla\\/5\\.0\\s.+\\sSeaMonkey\\/\\d+\\.\\d+/H'
  437. local user_agent_postbox = [[User-Agent=/^\s*Mozilla\/5\.0\s\([^)]+\)\sGecko\/\d+\sPostboxApp\/\d+(?:\.\d+){2,3}$/H]]
  438. local user_agent_mozilla = string.format('(%s) & !(%s) & !(%s) & !(%s)', user_agent_mozilla5, user_agent_thunderbird, user_agent_seamonkey, user_agent_postbox)
  439. -- Mozilla based common Message-ID template
  440. local mozilla_msgid_common = 'Message-ID=/^\\s*<[\\dA-F]{8}\\.\\d{1,7}\\@([^>\\.]+\\.)+[^>\\.]+>$/H'
  441. local mozilla_msgid_common_sec = 'Message-ID=/^\\s*<[\\da-f]{8}-([\\da-f]{4}-){3}[\\da-f]{12}\\@([^>\\.]+\\.)+[^>\\.]+>$/H'
  442. local mozilla_msgid = 'Message-ID=/^\\s*<(3[3-9A-F]|4[\\dA-F]|5[\\dA-F])[\\dA-F]{6}\\.(\\d0){1,4}\\d\\@([^>\\.]+\\.)+[^>\\.]+>$/H'
  443. -- Summary rule for forged Mozilla Mail Message-ID header
  444. reconf['FORGED_MUA_MOZILLA_MAIL_MSGID'] = {
  445. re = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_mozilla, mozilla_msgid_common, mozilla_msgid, unusable_msgid),
  446. score = 4.0,
  447. description = 'Message pretends to be send from Mozilla Mail but has forged Message-ID',
  448. group = 'mua'
  449. }
  450. reconf['FORGED_MUA_MOZILLA_MAIL_MSGID_UNKNOWN'] = {
  451. re = string.format('(%s) & !(%s) & !(%s) & !(%s)', user_agent_mozilla, mozilla_msgid_common, mozilla_msgid, unusable_msgid),
  452. score = 2.5,
  453. description = 'Message pretends to be send from Mozilla Mail but has forged Message-ID',
  454. group = 'mua'
  455. }
  456. -- Summary rule for forged Thunderbird Message-ID header
  457. reconf['FORGED_MUA_THUNDERBIRD_MSGID'] = {
  458. re = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_thunderbird, mozilla_msgid_common, mozilla_msgid, unusable_msgid),
  459. score = 4.0,
  460. description = 'Forged mail pretending to be from Mozilla Thunderbird but has forged Message-ID',
  461. group = 'mua'
  462. }
  463. reconf['FORGED_MUA_THUNDERBIRD_MSGID_UNKNOWN'] = {
  464. re = string.format('(%s) & !((%s) | (%s)) & !(%s) & !(%s)', user_agent_thunderbird, mozilla_msgid_common, mozilla_msgid_common_sec, mozilla_msgid, unusable_msgid),
  465. score = 2.5,
  466. description = 'Forged mail pretending to be from Mozilla Thunderbird but has forged Message-ID',
  467. group = 'mua'
  468. }
  469. -- Summary rule for forged Seamonkey Message-ID header
  470. reconf['FORGED_MUA_SEAMONKEY_MSGID'] = {
  471. re = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_seamonkey, mozilla_msgid_common, mozilla_msgid, unusable_msgid),
  472. score = 4.0,
  473. description = 'Forged mail pretending to be from Mozilla Seamonkey but has forged Message-ID',
  474. group = 'mua'
  475. }
  476. reconf['FORGED_MUA_SEAMONKEY_MSGID_UNKNOWN'] = {
  477. re = string.format('(%s) & !((%s) | (%s)) & !(%s) & !(%s)', user_agent_seamonkey, mozilla_msgid_common, mozilla_msgid_common_sec, mozilla_msgid, unusable_msgid),
  478. score = 2.5,
  479. description = 'Forged mail pretending to be from Mozilla Seamonkey but has forged Message-ID',
  480. group = 'mua'
  481. }
  482. -- Summary rule for forged Postbox Message-ID header
  483. reconf['FORGED_MUA_POSTBOX_MSGID'] = {
  484. re = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_postbox, mozilla_msgid_common, mozilla_msgid, unusable_msgid),
  485. score = 4.0,
  486. description = 'Forged mail pretending to be from Postbox but has forged Message-ID',
  487. group = 'mua'
  488. }
  489. reconf['FORGED_MUA_POSTBOX_MSGID_UNKNOWN'] = {
  490. re = string.format('(%s) & !((%s) | (%s)) & !(%s) & !(%s)', user_agent_postbox, mozilla_msgid_common, mozilla_msgid_common_sec, mozilla_msgid, unusable_msgid),
  491. score = 2.5,
  492. description = 'Forged mail pretending to be from Postbox but has forged Message-ID',
  493. group = 'mua'
  494. }
  495. -- Message id validity
  496. local sane_msgid = 'Message-Id=/^<?[^<>\\\\ \\t\\n\\r\\x0b\\x80-\\xff]+\\@[^<>\\\\ \\t\\n\\r\\x0b\\x80-\\xff]+>?\\s*$/H'
  497. local msgid_comment = 'Message-Id=/\\(.*\\)/H'
  498. reconf['INVALID_MSGID'] = {
  499. re = string.format('(%s) & !((%s) | (%s))', has_mid, sane_msgid, msgid_comment),
  500. score = 1.7,
  501. description = 'Message id is incorrect',
  502. group = 'headers'
  503. }
  504. -- Only Content-Type header without other MIME headers
  505. local cd = 'header_exists(Content-Disposition)'
  506. local cte = 'header_exists(Content-Transfer-Encoding)'
  507. local ct = 'header_exists(Content-Type)'
  508. local mime_version = 'raw_header_exists(MIME-Version)'
  509. local ct_text_plain = 'content_type_is_type(text) & content_type_is_subtype(plain)'
  510. reconf['MIME_HEADER_CTYPE_ONLY'] = {
  511. re = string.format('!(%s) & !(%s) & (%s) & !(%s) & !(%s)', cd, cte, ct, mime_version, ct_text_plain),
  512. score = 2.0,
  513. description = 'Only Content-Type header without other MIME headers',
  514. group = 'headers'
  515. }
  516. -- Forged Exchange messages
  517. local msgid_dollars_ok = 'Message-Id=/[0-9a-f]{4,}\\$[0-9a-f]{4,}\\$[0-9a-f]{4,}\\@\\S+/H'
  518. local mimeole_ms = 'X-MimeOLE=/^Produced By Microsoft MimeOLE/H'
  519. local rcvd_with_exchange = 'Received=/with Microsoft Exchange Server/H'
  520. reconf['RATWARE_MS_HASH'] = {
  521. re = string.format('(%s) & !(%s) & !(%s)', msgid_dollars_ok, mimeole_ms, rcvd_with_exchange),
  522. score = 2.0,
  523. description = 'Forged Exchange messages',
  524. group = 'headers'
  525. }
  526. -- Reply-type in content-type
  527. reconf['STOX_REPLY_TYPE'] = {
  528. re = 'Content-Type=/text\\/plain; .* reply-type=original/H',
  529. score = 1.0,
  530. description = 'Reply-type in content-type',
  531. group = 'headers'
  532. }
  533. -- Fake Verizon headers
  534. local fhelo_verizon = 'X-Spam-Relays-Untrusted=/^[^\\]]+ helo=[^ ]+verizon\\.net /iH'
  535. local fhost_verizon = 'X-Spam-Relays-Untrusted=/^[^\\]]+ rdns=[^ ]+verizon\\.net /iH'
  536. reconf['FM_FAKE_HELO_VERIZON'] = {
  537. re = string.format('(%s) & !(%s)', fhelo_verizon, fhost_verizon),
  538. score = 2.0,
  539. description = 'Fake helo for verizon provider',
  540. group = 'headers'
  541. }
  542. -- Forged yahoo msgid
  543. local at_yahoo_msgid = 'Message-Id=/\\@yahoo\\.com\\b/iH'
  544. local from_yahoo_com = 'From=/\\@yahoo\\.com\\b/iH'
  545. reconf['FORGED_MSGID_YAHOO'] = {
  546. re = string.format('(%s) & !(%s)', at_yahoo_msgid, from_yahoo_com),
  547. score = 2.0,
  548. description = 'Forged yahoo msgid',
  549. group = 'headers'
  550. }
  551. -- Forged The Bat! MUA headers
  552. local thebat_mua_v1 = 'X-Mailer=/^The Bat! \\(v1\\./H'
  553. local ctype_has_boundary = 'Content-Type=/boundary/iH'
  554. local bat_boundary = 'Content-Type=/boundary=\\"?-{10}/H'
  555. local mailman_21 = 'X-Mailman-Version=/\\d/H'
  556. reconf['FORGED_MUA_THEBAT_BOUN'] = {
  557. re = string.format('(%s) & (%s) & !(%s) & !(%s)', thebat_mua_v1, ctype_has_boundary, bat_boundary, mailman_21),
  558. score = 2.0,
  559. description = 'Forged The Bat! MUA headers',
  560. group = 'headers'
  561. }
  562. -- Detect Mail.Ru web-mail
  563. local xm_mail_ru_mailer_1_0 = 'X-Mailer=/^Mail\\.Ru Mailer 1\\.0$/H'
  564. local rcvd_e_mail_ru = 'Received=/^(?:from \\[\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\] )?by e\\.mail\\.ru with HTTP;/mH'
  565. reconf['MAIL_RU_MAILER'] = {
  566. re = string.format('(%s) & (%s)', xm_mail_ru_mailer_1_0, rcvd_e_mail_ru),
  567. score = 0.0,
  568. description = 'Sent with Mail.Ru web-mail',
  569. group = 'headers'
  570. }
  571. -- Detect yandex.ru web-mail
  572. local xm_yandex_ru_mailer_5_0 = 'X-Mailer=/^Yamail \\[ http:\\/\\/yandex\\.ru \\] 5\\.0$/H'
  573. local rcvd_web_yandex_ru = 'Received=/^by web\\d{1,2}[a-z]\\.yandex\\.ru with HTTP;/mH'
  574. reconf['YANDEX_RU_MAILER'] = {
  575. re = string.format('(%s) & (%s)', xm_yandex_ru_mailer_5_0, rcvd_web_yandex_ru),
  576. score = 0.0,
  577. description = 'Sent with yandex.ru web-mail',
  578. group = 'headers'
  579. }
  580. -- Detect 1C v8.2 and v8.3 mailers
  581. reconf['MAILER_1C_8'] = {
  582. re = 'X-Mailer=/^1C:Enterprise 8\\.[23]$/H',
  583. score = 0.0,
  584. description = 'Sent with 1C:Enterprise 8',
  585. group = 'headers'
  586. }
  587. -- Detect rogue 'strongmail' MTA with IPv4 and '(-)' in Received line
  588. reconf['STRONGMAIL'] = {
  589. re = [[Received=/^from\s+strongmail\s+\(\[\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\]\) by \S+ \(-\); /mH]],
  590. score = 6.0,
  591. description = 'Sent via rogue "strongmail" MTA',
  592. group = 'headers'
  593. }
  594. -- Two received headers with ip addresses
  595. local double_ip_spam_1 = 'Received=/from \\[\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\] by \\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3} with/H'
  596. local double_ip_spam_2 = 'Received=/from\\s+\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\s+by\\s+\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3};/H'
  597. reconf['RCVD_DOUBLE_IP_SPAM'] = {
  598. re = string.format('(%s) | (%s)', double_ip_spam_1, double_ip_spam_2),
  599. score = 2.0,
  600. description = 'Two received headers with ip addresses',
  601. group = 'headers'
  602. }
  603. -- Quoted reply-to from yahoo (seems to be forged)
  604. local repto_quote = 'Reply-To=/\\".*\\"\\s*\\</H'
  605. reconf['REPTO_QUOTE_YAHOO'] = {
  606. re = string.format('(%s) & ((%s) | (%s))', repto_quote, from_yahoo_com, at_yahoo_msgid),
  607. score = 2.0,
  608. description = 'Quoted reply-to from yahoo (seems to be forged)',
  609. group = 'headers'
  610. }
  611. -- MUA definitions
  612. local xm_gnus = 'X-Mailer=/^Gnus v/H'
  613. local xm_msoe5 = 'X-Mailer=/^Microsoft Outlook Express 5/H'
  614. local xm_msoe6 = 'X-Mailer=/^Microsoft Outlook Express 6/H'
  615. local xm_moz4 = 'X-Mailer=/^Mozilla 4/H'
  616. local xm_skyri = 'X-Mailer=/^SKYRiXgreen/H'
  617. local xm_wwwmail = 'X-Mailer=/^WWW-Mail \\d/H'
  618. local ua_gnus = 'User-Agent=/^Gnus/H'
  619. local ua_knode = 'User-Agent=/^KNode/H'
  620. local ua_mutt = 'User-Agent=/^Mutt/H'
  621. local ua_pan = 'User-Agent=/^Pan/H'
  622. local ua_xnews = 'User-Agent=/^Xnews/H'
  623. local no_inr_yes_ref = string.format('(%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s)', xm_gnus, xm_msoe5, xm_msoe6, xm_moz4, xm_skyri, xm_wwwmail, ua_gnus, ua_knode, ua_mutt, ua_pan, ua_xnews)
  624. local subj_re = 'Subject=/^R[eE]:/H'
  625. local has_ref = '(header_exists(References) | header_exists(In-Reply-To))'
  626. local missing_ref = string.format('!(%s)', has_ref)
  627. -- Fake reply (has RE in subject, but has not References header)
  628. reconf['FAKE_REPLY_C'] = {
  629. re = string.format('(%s) & (%s) & (%s) & !(%s)', subj_re, missing_ref, no_inr_yes_ref, xm_msoe6),
  630. score = 6.0,
  631. description = 'Fake reply (has RE in subject, but has not References header)',
  632. group = 'subject'
  633. }
  634. -- Mime-OLE is needed but absent (e.g. fake Outlook or fake Ecxchange)
  635. local has_msmail_pri = 'header_exists(X-MSMail-Priority)'
  636. local has_mimeole = 'header_exists(X-MimeOLE)'
  637. local has_squirrelmail_in_mailer = 'X-Mailer=/SquirrelMail\\b/H'
  638. local has_office_version_in_mailer = [[X-Mailer=/^Microsoft (?:Office )?Outlook [12]\d\.0/]]
  639. reconf['MISSING_MIMEOLE'] = {
  640. re = string.format('(%s) & !(%s) & !(%s) & !(%s)',
  641. has_msmail_pri,
  642. has_mimeole,
  643. has_squirrelmail_in_mailer,
  644. has_office_version_in_mailer),
  645. score = 2.0,
  646. description = 'Mime-OLE is needed but absent (e.g. fake Outlook or fake Exchange)',
  647. group = 'headers'
  648. }
  649. -- Header delimiters
  650. local yandex_from = 'From=/\\@(yandex\\.ru|yandex\\.net|ya\\.ru)/iX'
  651. local yandex_x_envelope_from = 'X-Envelope-From=/\\@(yandex\\.ru|yandex\\.net|ya\\.ru)/iX'
  652. local yandex_return_path = 'Return-Path=/\\@(yandex\\.ru|yandex\\.net|ya\\.ru)/iX'
  653. local yandex_received = 'Received=/^\\s*from \\S+\\.(yandex\\.ru|yandex\\.net)/mH'
  654. local yandex = string.format('(%s) & ((%s) | (%s) | (%s))', yandex_received, yandex_from, yandex_x_envelope_from, yandex_return_path)
  655. -- Tabs as delimiters between header names and header values
  656. function check_header_delimiter_tab(task, header_name)
  657. for _,rh in ipairs(task:get_header_full(header_name)) do
  658. if rh['tab_separated'] then return true end
  659. end
  660. return false
  661. end
  662. reconf['HEADER_FROM_DELIMITER_TAB'] = {
  663. re = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(From)', yandex),
  664. score = 1.0,
  665. description = 'Header From begins with tab',
  666. group = 'headers'
  667. }
  668. reconf['HEADER_TO_DELIMITER_TAB'] = {
  669. re = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(To)', yandex),
  670. score = 1.0,
  671. description = 'Header To begins with tab',
  672. group = 'headers'
  673. }
  674. reconf['HEADER_CC_DELIMITER_TAB'] = {
  675. re = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(Cc)', yandex),
  676. score = 1.0,
  677. description = 'Header To begins with tab',
  678. group = 'headers'
  679. }
  680. reconf['HEADER_REPLYTO_DELIMITER_TAB'] = {
  681. re = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(Reply-To)', yandex),
  682. score = 1.0,
  683. description = 'Header Reply-To begins with tab',
  684. group = 'headers'
  685. }
  686. reconf['HEADER_DATE_DELIMITER_TAB'] = {
  687. re = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(Date)', yandex),
  688. score = 1.0,
  689. description = 'Header Date begins with tab',
  690. group = 'headers'
  691. }
  692. -- Empty delimiters between header names and header values
  693. function check_header_delimiter_empty(task, header_name)
  694. for _,rh in ipairs(task:get_header_full(header_name)) do
  695. if rh['empty_separator'] then return true end
  696. end
  697. return false
  698. end
  699. reconf['HEADER_FROM_EMPTY_DELIMITER'] = {
  700. re = string.format('(%s)', 'check_header_delimiter_empty(From)'),
  701. score = 1.0,
  702. description = 'Header From has no delimiter between header name and header value',
  703. group = 'headers'
  704. }
  705. reconf['HEADER_TO_EMPTY_DELIMITER'] = {
  706. re = string.format('(%s)', 'check_header_delimiter_empty(To)'),
  707. score = 1.0,
  708. description = 'Header To has no delimiter between header name and header value',
  709. group = 'headers'
  710. }
  711. reconf['HEADER_CC_EMPTY_DELIMITER'] = {
  712. re = string.format('(%s)', 'check_header_delimiter_empty(Cc)'),
  713. score = 1.0,
  714. description = 'Header Cc has no delimiter between header name and header value',
  715. group = 'headers'
  716. }
  717. reconf['HEADER_REPLYTO_EMPTY_DELIMITER'] = {
  718. re = string.format('(%s)', 'check_header_delimiter_empty(Reply-To)'),
  719. score = 1.0,
  720. description = 'Header Reply-To has no delimiter between header name and header value',
  721. group = 'headers'
  722. }
  723. reconf['HEADER_DATE_EMPTY_DELIMITER'] = {
  724. re = string.format('(%s)', 'check_header_delimiter_empty(Date)'),
  725. score = 1.0,
  726. description = 'Header Date has no delimiter between header name and header value',
  727. group = 'headers'
  728. }
  729. -- Definitions of received headers regexp
  730. reconf['RCVD_ILLEGAL_CHARS'] = {
  731. re = 'Received=/[\\x80-\\xff]/X',
  732. score = 4.0,
  733. description = 'Header Received has raw illegal character',
  734. group = 'headers'
  735. }
  736. local MAIL_RU_Return_Path = 'Return-path=/^\\s*<.+\\@mail\\.ru>$/iX'
  737. local MAIL_RU_X_Envelope_From = 'X-Envelope-From=/^\\s*<.+\\@mail\\.ru>$/iX'
  738. local MAIL_RU_From = 'From=/\\@mail\\.ru>?$/iX'
  739. local MAIL_RU_Received = 'Received=/from mail\\.ru \\(/mH'
  740. reconf['FAKE_RECEIVED_mail_ru'] = {
  741. re = string.format('(%s) & !(((%s) | (%s)) & (%s))', MAIL_RU_Received, MAIL_RU_Return_Path, MAIL_RU_X_Envelope_From, MAIL_RU_From),
  742. score = 4.0,
  743. description = 'Fake helo mail.ru in header Received from non mail.ru sender address',
  744. group = 'headers'
  745. }
  746. local GMAIL_COM_Return_Path = 'Return-path=/^\\s*<.+\\@gmail\\.com>$/iX'
  747. local GMAIL_COM_X_Envelope_From = 'X-Envelope-From=/^\\s*<.+\\@gmail\\.com>$/iX'
  748. local GMAIL_COM_From = 'From=/\\@gmail\\.com>?$/iX'
  749. local UKR_NET_Return_Path = 'Return-path=/^\\s*<.+\\@ukr\\.net>$/iX'
  750. local UKR_NET_X_Envelope_From = 'X-Envelope-From=/^\\s*<.+\\@ukr\\.net>$/iX'
  751. local UKR_NET_From = 'From=/\\@ukr\\.net>?$/iX'
  752. local RECEIVED_smtp_yandex_ru_1 = 'Received=/from \\[\\d+\\.\\d+\\.\\d+\\.\\d+\\] \\((port=\\d+ )?helo=smtp\\.yandex\\.ru\\)/iX'
  753. local RECEIVED_smtp_yandex_ru_2 = 'Received=/from \\[UNAVAILABLE\\] \\(\\[\\d+\\.\\d+\\.\\d+\\.\\d+\\]:\\d+ helo=smtp\\.yandex\\.ru\\)/iX'
  754. local RECEIVED_smtp_yandex_ru_3 = 'Received=/from \\S+ \\(\\[\\d+\\.\\d+\\.\\d+\\.\\d+\\]:\\d+ helo=smtp\\.yandex\\.ru\\)/iX'
  755. local RECEIVED_smtp_yandex_ru_4 = 'Received=/from \\[\\d+\\.\\d+\\.\\d+\\.\\d+\\] \\(account \\S+ HELO smtp\\.yandex\\.ru\\)/iX'
  756. local RECEIVED_smtp_yandex_ru_5 = 'Received=/from smtp\\.yandex\\.ru \\(\\[\\d+\\.\\d+\\.\\d+\\.\\d+\\]\\)/iX'
  757. local RECEIVED_smtp_yandex_ru_6 = 'Received=/from smtp\\.yandex\\.ru \\(\\S+ \\[\\d+\\.\\d+\\.\\d+\\.\\d+\\]\\)/iX'
  758. local RECEIVED_smtp_yandex_ru_7 = 'Received=/from \\S+ \\(HELO smtp\\.yandex\\.ru\\) \\(\\S+\\@\\d+\\.\\d+\\.\\d+\\.\\d+\\)/iX'
  759. local RECEIVED_smtp_yandex_ru_8 = 'Received=/from \\S+ \\(HELO smtp\\.yandex\\.ru\\) \\(\\d+\\.\\d+\\.\\d+\\.\\d+\\)/iX'
  760. local RECEIVED_smtp_yandex_ru_9 = 'Received=/from \\S+ \\(\\[\\d+\\.\\d+\\.\\d+\\.\\d+\\] helo=smtp\\.yandex\\.ru\\)/iX'
  761. reconf['FAKE_RECEIVED_smtp_yandex_ru'] = {
  762. re = string.format('(((%s) & ((%s) | (%s))) | ((%s) & ((%s) | (%s))) | ((%s) & ((%s) | (%s)))) & (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s)', MAIL_RU_From, MAIL_RU_Return_Path, MAIL_RU_X_Envelope_From, GMAIL_COM_From, GMAIL_COM_Return_Path, GMAIL_COM_X_Envelope_From, UKR_NET_From, UKR_NET_Return_Path, UKR_NET_X_Envelope_From, RECEIVED_smtp_yandex_ru_1, RECEIVED_smtp_yandex_ru_2, RECEIVED_smtp_yandex_ru_3, RECEIVED_smtp_yandex_ru_4, RECEIVED_smtp_yandex_ru_5, RECEIVED_smtp_yandex_ru_6, RECEIVED_smtp_yandex_ru_7, RECEIVED_smtp_yandex_ru_8, RECEIVED_smtp_yandex_ru_9),
  763. score = 4.0,
  764. description = 'Fake smtp.yandex.ru Received',
  765. group = 'headers'
  766. }
  767. reconf['FORGED_GENERIC_RECEIVED'] = {
  768. re = 'Received=/^\\s*(.+\\n)*from \\[\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\] by (([\\w\\d-]+\\.)+[a-zA-Z]{2,6}|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}); \\w{3}, \\d+ \\w{3} 20\\d\\d \\d\\d\\:\\d\\d\\:\\d\\d [+-]\\d\\d\\d0/X',
  769. score = 3.6,
  770. description = 'Forged generic Received',
  771. group = 'headers'
  772. }
  773. reconf['FORGED_GENERIC_RECEIVED2'] = {
  774. re = 'Received=/^\\s*(.+\\n)*from \\[\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\] by ([\\w\\d-]+\\.)+[a-z]{2,6} id [\\w\\d]{12}; \\w{3}, \\d+ \\w{3} 20\\d\\d \\d\\d\\:\\d\\d\\:\\d\\d [+-]\\d\\d\\d0/X',
  775. score = 3.6,
  776. description = 'Forged generic Received',
  777. group = 'headers'
  778. }
  779. reconf['FORGED_GENERIC_RECEIVED3'] = {
  780. re = 'Received=/^\\s*(.+\\n)*by \\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3} with SMTP id [a-zA-Z]{14}\\.\\d{13};[\\r\\n\\s]*\\w{3}, \\d+ \\w{3} 20\\d\\d \\d\\d\\:\\d\\d\\:\\d\\d [+-]\\d\\d\\d0 \\(GMT\\)/X',
  781. score = 3.6,
  782. description = 'Forged generic Received',
  783. group = 'headers'
  784. }
  785. reconf['FORGED_GENERIC_RECEIVED4'] = {
  786. re = 'Received=/^\\s*(.+\\n)*from localhost by \\S+;\\s+\\w{3}, \\d+ \\w{3} 20\\d\\d \\d\\d\\:\\d\\d\\:\\d\\d [+-]\\d\\d\\d0[\\s\\r\\n]*$/X',
  787. score = 3.6,
  788. description = 'Forged generic Received',
  789. group = 'headers'
  790. }
  791. reconf['INVALID_POSTFIX_RECEIVED'] = {
  792. re = 'Received=/ \\(Postfix\\) with ESMTP id [A-Z\\d]+([\\s\\r\\n]+for <\\S+?>)?;[\\s\\r\\n]*[A-Z][a-z]{2}, \\d{1,2} [A-Z][a-z]{2} \\d\\d\\d\\d \\d\\d:\\d\\d:\\d\\d [\\+\\-]\\d\\d\\d\\d$/X',
  793. score = 3.0,
  794. description = 'Invalid Postfix Received',
  795. group = 'headers'
  796. }
  797. reconf['X_PHP_FORGED_0X'] = {
  798. re = "X-PHP-Originating-Script=/^0\\d/X",
  799. score = 4.0,
  800. description = "X-PHP-Originating-Script header appears forged",
  801. group = 'headers'
  802. }
  803. reconf['GOOGLE_FORWARDING_MID_MISSING'] = {
  804. re = "Message-ID=/SMTPIN_ADDED_MISSING\\@mx\\.google\\.com>$/X",
  805. score = 2.5,
  806. description = "Message was missing Message-ID pre-forwarding",
  807. group = 'headers'
  808. }
  809. reconf['GOOGLE_FORWARDING_MID_BROKEN'] = {
  810. re = "Message-ID=/SMTPIN_ADDED_BROKEN\\@mx\\.google\\.com>$/X",
  811. score = 1.7,
  812. description = "Message had invalid Message-ID pre-forwarding",
  813. group = 'headers'
  814. }
  815. reconf['CTE_CASE'] = {
  816. re = 'Content-Transfer-Encoding=/^[78]B/X',
  817. description = '[78]Bit .vs. [78]bit',
  818. score = 0.5,
  819. group = 'headers'
  820. }
  821. reconf['HAS_INTERSPIRE_SIG'] = {
  822. re = string.format('((%s) & (%s) & (%s) & (%s)) | (%s)',
  823. 'header_exists(X-Mailer-LID)',
  824. 'header_exists(X-Mailer-RecptId)',
  825. 'header_exists(X-Mailer-SID)',
  826. 'header_exists(X-Mailer-Sent-By)',
  827. 'List-Unsubscribe=/\\/unsubscribe\\.php\\?M=[^&]+&C=[^&]+&L=[^&]+&N=[^>]+>$/Xi'),
  828. description = "Has Interspire fingerprint",
  829. score = 1.0,
  830. group = 'headers'
  831. }
  832. reconf['CT_EXTRA_SEMI'] = {
  833. re = 'Content-Type=/;$/X',
  834. description = 'Content-Type ends with a semi-colon',
  835. score = 1.0,
  836. group = 'headers'
  837. }
  838. reconf['SUBJECT_ENDS_EXCLAIM'] = {
  839. re = 'Subject=/!\\s*$/H',
  840. description = 'Subject ends with an exclaimation',
  841. score = 0.0,
  842. group = 'headers'
  843. }
  844. reconf['SUBJECT_HAS_EXCLAIM'] = {
  845. re = string.format('%s & !%s', 'Subject=/!/H', 'Subject=/!\\s*$/H'),
  846. description = 'Subject contains an exclaimation',
  847. score = 0.0,
  848. group = 'headers'
  849. }
  850. reconf['SUBJECT_ENDS_QUESTION'] = {
  851. re = 'Subject=/\\?\\s*$/Hu',
  852. description = 'Subject ends with a question',
  853. score = 1.0,
  854. group = 'headers'
  855. }
  856. reconf['SUBJECT_HAS_QUESTION'] = {
  857. re = string.format('%s & !%s', 'Subject=/\\?/H', 'Subject=/\\?\\s*$/Hu'),
  858. description = 'Subject contains a question',
  859. score = 0.0,
  860. group = 'headers'
  861. }
  862. reconf['SUBJECT_HAS_CURRENCY'] = {
  863. re = 'Subject=/[$€$¢¥₽]/Hu',
  864. description = 'Subject contains currency',
  865. score = 1.0,
  866. group = 'headers'
  867. }
  868. reconf['SUBJECT_ENDS_SPACES'] = {
  869. re = 'Subject=/\\s+$/H',
  870. description = 'Subject ends with space characters',
  871. score = 0.5,
  872. group = 'headers'
  873. }
  874. reconf['HAS_ORG_HEADER'] = {
  875. re = string.format('%s || %s', 'header_exists(Organization)', 'header_exists(Organisation)'),
  876. description = 'Has Organization header',
  877. score = 0.0,
  878. group = 'headers'
  879. }
  880. reconf['X_PHPOS_FAKE'] = {
  881. re = 'X-PHP-Originating-Script=/^\\d{7}:/Hi',
  882. description = 'Fake X-PHP-Originating-Script header',
  883. score = 3.0,
  884. group = 'headers'
  885. }
  886. reconf['HAS_XOIP'] = {
  887. re = "header_exists('X-Originating-IP')",
  888. description = "Has X-Originating-IP header",
  889. score = 0.0,
  890. group = 'headers'
  891. }
  892. reconf['HAS_LIST_UNSUB'] = {
  893. re = string.format('%s', 'header_exists(List-Unsubscribe)'),
  894. description = 'Has List-Unsubscribe header',
  895. score = -0.01,
  896. group = 'headers'
  897. }
  898. reconf['HAS_GUC_PROXY_URI'] = {
  899. re = '/\\.googleusercontent\\.com\\/proxy/{url}i',
  900. description = 'Has googleusercontent.com proxy URI',
  901. score = 0.01,
  902. group = 'experimental'
  903. }
  904. reconf['HAS_GOOGLE_REDIR'] = {
  905. re = '/\\.google\\.com\\/url\\?/{url}i',
  906. description = 'Has google.com/url redirection',
  907. score = 0.01,
  908. group = 'experimental'
  909. }
  910. reconf['XM_UA_NO_VERSION'] = {
  911. re = string.format('(!%s && !%s) && (%s || %s)',
  912. 'X-Mailer=/https?:/H',
  913. 'User-Agent=/https?:/H',
  914. 'X-Mailer=/^[^0-9]+$/H',
  915. 'User-Agent=/^[^0-9]+$/H'),
  916. description = 'X-Mailer/User-Agent has no version',
  917. score = 0.01,
  918. group = 'experimental'
  919. }