Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

headers.lua 39KB

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