You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074
  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-Recipient:;">
  106. -- To: "undisclosed-recipients (utajeni adresati)": ;
  107. -- To: Undisclosed recipients:
  108. -- but do not match:
  109. -- Undisclosed Recipient <user@example.org>
  110. re = [[To=/^<?"?undisclosed[- ]recipients?\b.*:/i{header}]],
  111. score = 3.0,
  112. description = 'Recipients are absent or undisclosed',
  113. group = 'headers',
  114. mime_only = true,
  115. }
  116. -- Detects missing Message-ID
  117. local has_mid = 'header_exists(Message-Id)'
  118. reconf['MISSING_MID'] = {
  119. re = '!header_exists(Message-Id)',
  120. score = 2.5,
  121. description = 'Message-ID header is missing',
  122. group = 'headers',
  123. mime_only = true,
  124. }
  125. -- Received seems to be fake
  126. reconf['R_RCVD_SPAMBOTS'] = {
  127. re = 'Received=/^from \\[\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\] by [-.\\w+]{5,255}; [SMTWF][a-z][a-z],' ..
  128. ' [\\s\\d]?\\d [JFMAJSOND][a-z][a-z] \\d{4} \\d{2}:\\d{2}:\\d{2} [-+]\\d{4}$/mH',
  129. score = 3.0,
  130. description = 'Spambots signatures in received headers',
  131. group = 'headers',
  132. mime_only = true,
  133. }
  134. -- Charset is missing in message
  135. reconf['R_MISSING_CHARSET'] = {
  136. re = string.format('!is_empty_body() & content_type_is_type(text) & content_type_is_subtype(plain) & !content_type_has_param(charset) & !%s',
  137. 'compare_transfer_encoding(7bit)'),
  138. score = 0.5,
  139. description = 'Charset header is missing',
  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 header is unnecessarily encoded in base64',
  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 header is unnecessarily encoded in quoted-printable',
  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 header is unnecessarily encoded in base64',
  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 header is unnecessarily encoded in quoted-printable',
  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 header is unnecessarily encoded in base64',
  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 header is unnecessarily encoded in quoted-printable',
  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 header is unnecessarily encoded in base64',
  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 header is unnecessarily encoded in quoted-printable',
  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 header 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 header 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. -- https://bazaar.launchpad.net/~mailman-coders/mailman/2.1/view/head:/Mailman/Utils.py#L811
  291. local mailman_msgid = [[Message-ID=/^<mailman\.\d+\.\d+\.\d+\.[-+.:=\w]+@[-a-zA-Z\d.]+>$/H]]
  292. -- Message id seems to be forged
  293. local unusable_msgid = string.format('(%s | %s | %s | %s | %s | %s)',
  294. lyris_ezml_remailer, wacky_sendmail_version,
  295. iplanet_messaging_server, hotmail_baydav_msgid, sympatico_msgid, mailman_msgid)
  296. -- Outlook express data seems to be forged
  297. local forged_oe = string.format('(%s & !%s & !%s & !%s)', oe_mua, oe_msgid_1, oe_msgid_2, unusable_msgid)
  298. -- Outlook specific headers
  299. local outlook_dollars_mua = 'X-Mailer=/^Microsoft Outlook(?: 8| CWS, Build 9|, Build 10)\\./H'
  300. local outlook_dollars_other = 'Message-Id=/^<?\\!\\~\\!>?/H'
  301. local vista_msgid = 'Message-Id=/^<?[A-F\\d]{32}\\@\\S+>?$/H'
  302. local ims_msgid = 'Message-Id=/^<?[A-F\\d]{36,40}\\@\\S+>?$/H'
  303. -- Forged outlook headers
  304. local forged_outlook_dollars = string.format('(%s & !%s & !%s & !%s & !%s & !%s)',
  305. outlook_dollars_mua, oe_msgid_2, outlook_dollars_other, vista_msgid, ims_msgid, unusable_msgid)
  306. -- Outlook versions that should be excluded from summary rule
  307. local fmo_excl_o3416 = 'X-Mailer=/^Microsoft Outlook, Build 10.0.3416$/H'
  308. local fmo_excl_oe3790 = 'X-Mailer=/^Microsoft Outlook Express 6.00.3790.3959$/H'
  309. -- Summary rule for forged outlook
  310. reconf['FORGED_MUA_OUTLOOK'] = {
  311. re = string.format('(%s | %s) & !%s & !%s & !%s',
  312. forged_oe, forged_outlook_dollars, fmo_excl_o3416, fmo_excl_oe3790, vista_msgid),
  313. score = 3.0,
  314. description = 'Forged Outlook MUA',
  315. group = 'mua'
  316. }
  317. -- HTML outlook signs
  318. local mime_html = 'content_type_is_type(text) & content_type_is_subtype(/.?html/)'
  319. local tag_exists_html = 'has_html_tag(html)'
  320. local tag_exists_head = 'has_html_tag(head)'
  321. local tag_exists_meta = 'has_html_tag(meta)'
  322. local tag_exists_body = 'has_html_tag(body)'
  323. reconf['FORGED_OUTLOOK_TAGS'] = {
  324. re = string.format('!%s & %s & %s & !(%s & %s & %s & %s)',
  325. yahoo_bulk, any_outlook_mua, mime_html, tag_exists_html, tag_exists_head,
  326. tag_exists_meta, tag_exists_body),
  327. score = 2.1,
  328. description = "Message pretends to be send from Outlook but has 'strange' tags",
  329. group = 'headers'
  330. }
  331. -- Forged OE/MSO boundary
  332. reconf['SUSPICIOUS_BOUNDARY'] = {
  333. re = 'Content-Type=/^\\s*multipart.+boundary="----=_NextPart_000_[A-Z\\d]{4}_(00EBFFA4|0102FFA4|32C6FFA4|3302FFA4)\\.[A-Z\\d]{8}"[\\r\\n]*$/siX',
  334. score = 5.0,
  335. description = 'Suspicious boundary in Content-Type header',
  336. group = 'mua'
  337. }
  338. -- Forged OE/MSO boundary
  339. reconf['SUSPICIOUS_BOUNDARY2'] = {
  340. re = 'Content-Type=/^\\s*multipart.+boundary="----=_NextPart_000_[A-Z\\d]{4}_(01C6527E)\\.[A-Z\\d]{8}"[\\r\\n]*$/siX',
  341. score = 4.0,
  342. description = 'Suspicious boundary in Content-Type header',
  343. group = 'mua'
  344. }
  345. -- Forged OE/MSO boundary
  346. reconf['SUSPICIOUS_BOUNDARY3'] = {
  347. re = 'Content-Type=/^\\s*multipart.+boundary="-----000-00\\d\\d-01C[\\dA-F]{5}-[\\dA-F]{8}"[\\r\\n]*$/siX',
  348. score = 3.0,
  349. description = 'Suspicious boundary in Content-Type header',
  350. group = 'mua'
  351. }
  352. -- Forged OE/MSO boundary
  353. 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'
  354. local suspicious_boundary_01C4_date = 'Date=/^\\s*\\w\\w\\w,\\s+\\d+\\s+\\w\\w\\w 20(0[56789]|1\\d)/'
  355. reconf['SUSPICIOUS_BOUNDARY4'] = {
  356. re = string.format('(%s) & (%s)', suspicious_boundary_01C4, suspicious_boundary_01C4_date),
  357. score = 4.0,
  358. description = 'Suspicious boundary in Content-Type header',
  359. group = 'mua'
  360. }
  361. -- Detect forged The Bat! headers
  362. -- The Bat! X-Mailer header
  363. local thebat_mua_any = 'X-Mailer=/^\\s*The Bat!/H'
  364. -- The Bat! common Message-ID template
  365. local thebat_msgid_common = 'Message-ID=/^<?\\d+\\.\\d+\\@\\S+>?$/mH'
  366. -- Correct The Bat! Message-ID template
  367. 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'
  368. -- Summary rule for forged The Bat! Message-ID header
  369. reconf['FORGED_MUA_THEBAT_MSGID'] = {
  370. re = string.format('(%s) & !(%s) & (%s) & !(%s)', thebat_mua_any, thebat_msgid, thebat_msgid_common, unusable_msgid),
  371. score = 4.0,
  372. description = 'Message pretends to be send from The Bat! but has forged Message-ID',
  373. group = 'mua'
  374. }
  375. -- Summary rule for forged The Bat! Message-ID header with unknown template
  376. reconf['FORGED_MUA_THEBAT_MSGID_UNKNOWN'] = {
  377. re = string.format('(%s) & !(%s) & !(%s) & !(%s)', thebat_mua_any, thebat_msgid, thebat_msgid_common, unusable_msgid),
  378. score = 3.0,
  379. description = 'Message pretends to be send from The Bat! but has forged Message-ID',
  380. group = 'mua'
  381. }
  382. -- Detect forged KMail headers
  383. -- KMail User-Agent header
  384. local kmail_mua = 'User-Agent=/^\\s*KMail\\/1\\.\\d+\\.\\d+/H'
  385. -- KMail common Message-ID template
  386. local kmail_msgid_common = 'Message-Id=/^<?\\s*\\d+\\.\\d+\\.\\S+\\@\\S+>?$/mH'
  387. -- Summary rule for forged KMail Message-ID header with unknown template
  388. reconf['FORGED_MUA_KMAIL_MSGID_UNKNOWN'] = {
  389. re = string.format('(%s) & !(%s) & !(%s)', kmail_mua, kmail_msgid_common, unusable_msgid),
  390. score = 2.5,
  391. description = 'Message pretends to be send from KMail but has forged Message-ID',
  392. group = 'mua'
  393. }
  394. -- Detect forged Opera Mail headers
  395. -- Opera Mail User-Agent header
  396. local opera1x_mua = 'User-Agent=/^\\s*Opera Mail\\/1[01]\\.\\d+ /H'
  397. -- Opera Mail Message-ID template
  398. local opera1x_msgid = 'Message-ID=/^<?op\\.[a-z\\d]{14}\\@\\S+>?$/H'
  399. -- Rule for forged Opera Mail Message-ID header
  400. reconf['FORGED_MUA_OPERA_MSGID'] = {
  401. re = string.format('(%s) & !(%s) & !(%s)', opera1x_mua, opera1x_msgid, unusable_msgid),
  402. score = 4.0,
  403. description = 'Message pretends to be send from Opera Mail but has forged Message-ID',
  404. group = 'mua'
  405. }
  406. -- Detect forged Mozilla Mail/Thunderbird/Seamonkey/Postbox headers
  407. -- Mozilla based X-Mailer
  408. local user_agent_mozilla5 = 'User-Agent=/^\\s*Mozilla\\/5\\.0/H'
  409. local user_agent_thunderbird = 'User-Agent=/^\\s*(Thunderbird|Mozilla Thunderbird|Mozilla\\/.*Gecko\\/.*(Thunderbird|Betterbird|Icedove)\\/)/H'
  410. local user_agent_seamonkey = 'User-Agent=/^\\s*Mozilla\\/5\\.0\\s.+\\sSeaMonkey\\/\\d+\\.\\d+/H'
  411. local user_agent_postbox = [[User-Agent=/^\s*Mozilla\/5\.0\s\([^)]+\)\sGecko\/\d+\sPostboxApp\/\d+(?:\.\d+){2,3}$/H]]
  412. local user_agent_mozilla = string.format('(%s) & !(%s) & !(%s) & !(%s)', user_agent_mozilla5, user_agent_thunderbird,
  413. user_agent_seamonkey, user_agent_postbox)
  414. -- Mozilla based common Message-ID template
  415. local mozilla_msgid_common = 'Message-ID=/^\\s*<[\\dA-F]{8}\\.\\d{1,7}\\@([^>\\.]+\\.)+[^>\\.]+>$/H'
  416. local mozilla_msgid_common_sec = 'Message-ID=/^\\s*<[\\da-f]{8}-([\\da-f]{4}-){3}[\\da-f]{12}\\@([^>\\.]+\\.)+[^>\\.]+>$/H'
  417. local mozilla_msgid = 'Message-ID=/^\\s*<(3[3-9A-F]|[4-9A-F][\\dA-F])[\\dA-F]{6}\\.(\\d0){1,4}\\d\\@([^>\\.]+\\.)+[^>\\.]+>$/H'
  418. -- Summary rule for forged Mozilla Mail Message-ID header
  419. reconf['FORGED_MUA_MOZILLA_MAIL_MSGID'] = {
  420. re = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_mozilla, mozilla_msgid_common, mozilla_msgid,
  421. unusable_msgid),
  422. score = 4.0,
  423. description = 'Message pretends to be send from Mozilla Mail but has forged Message-ID',
  424. group = 'mua'
  425. }
  426. reconf['FORGED_MUA_MOZILLA_MAIL_MSGID_UNKNOWN'] = {
  427. re = string.format('(%s) & !(%s) & !(%s) & !(%s)', user_agent_mozilla, mozilla_msgid_common, mozilla_msgid,
  428. unusable_msgid),
  429. score = 2.5,
  430. description = 'Message pretends to be send from Mozilla Mail but has forged Message-ID',
  431. group = 'mua'
  432. }
  433. -- Summary rule for forged Thunderbird Message-ID header
  434. reconf['FORGED_MUA_THUNDERBIRD_MSGID'] = {
  435. re = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_thunderbird, mozilla_msgid_common, mozilla_msgid,
  436. unusable_msgid),
  437. score = 4.0,
  438. description = 'Forged mail pretending to be from Mozilla Thunderbird but has forged Message-ID',
  439. group = 'mua'
  440. }
  441. reconf['FORGED_MUA_THUNDERBIRD_MSGID_UNKNOWN'] = {
  442. re = string.format('(%s) & !((%s) | (%s)) & !(%s) & !(%s)', user_agent_thunderbird, mozilla_msgid_common,
  443. mozilla_msgid_common_sec, mozilla_msgid, unusable_msgid),
  444. score = 2.5,
  445. description = 'Forged mail pretending to be from Mozilla Thunderbird but has forged Message-ID',
  446. group = 'mua'
  447. }
  448. -- Summary rule for forged Seamonkey Message-ID header
  449. reconf['FORGED_MUA_SEAMONKEY_MSGID'] = {
  450. re = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_seamonkey, mozilla_msgid_common, mozilla_msgid,
  451. unusable_msgid),
  452. score = 4.0,
  453. description = 'Forged mail pretending to be from Mozilla Seamonkey but has forged Message-ID',
  454. group = 'mua'
  455. }
  456. reconf['FORGED_MUA_SEAMONKEY_MSGID_UNKNOWN'] = {
  457. re = string.format('(%s) & !((%s) | (%s)) & !(%s) & !(%s)', user_agent_seamonkey, mozilla_msgid_common,
  458. mozilla_msgid_common_sec, mozilla_msgid, unusable_msgid),
  459. score = 2.5,
  460. description = 'Forged mail pretending to be from Mozilla Seamonkey but has forged Message-ID',
  461. group = 'mua'
  462. }
  463. -- Summary rule for forged Postbox Message-ID header
  464. reconf['FORGED_MUA_POSTBOX_MSGID'] = {
  465. re = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_postbox, mozilla_msgid_common, mozilla_msgid,
  466. unusable_msgid),
  467. score = 4.0,
  468. description = 'Forged mail pretending to be from Postbox but has forged Message-ID',
  469. group = 'mua'
  470. }
  471. reconf['FORGED_MUA_POSTBOX_MSGID_UNKNOWN'] = {
  472. re = string.format('(%s) & !((%s) | (%s)) & !(%s) & !(%s)', user_agent_postbox, mozilla_msgid_common,
  473. mozilla_msgid_common_sec, mozilla_msgid, unusable_msgid),
  474. score = 2.5,
  475. description = 'Forged mail pretending to be from Postbox but has forged Message-ID',
  476. group = 'mua'
  477. }
  478. -- Message id validity
  479. local sane_msgid = 'Message-Id=/^<?[^<>\\\\ \\t\\n\\r\\x0b\\x80-\\xff]+\\@[^<>\\\\ \\t\\n\\r\\x0b\\x80-\\xff]+>?\\s*$/H'
  480. local msgid_comment = 'Message-Id=/\\(.*\\)/H'
  481. reconf['INVALID_MSGID'] = {
  482. re = string.format('(%s) & !((%s) | (%s))', has_mid, sane_msgid, msgid_comment),
  483. score = 1.7,
  484. description = 'Message-ID header is incorrect',
  485. group = 'headers'
  486. }
  487. -- Only Content-Type header without other MIME headers
  488. local cd = 'header_exists(Content-Disposition)'
  489. local cte = 'header_exists(Content-Transfer-Encoding)'
  490. local ct = 'header_exists(Content-Type)'
  491. local mime_version = 'raw_header_exists(MIME-Version)'
  492. local ct_text_plain = 'content_type_is_type(text) & content_type_is_subtype(plain)'
  493. reconf['MIME_HEADER_CTYPE_ONLY'] = {
  494. re = string.format('!(%s) & !(%s) & (%s) & !(%s) & !(%s)', cd, cte, ct, mime_version, ct_text_plain),
  495. score = 2.0,
  496. description = 'Only Content-Type header without other MIME headers',
  497. group = 'headers'
  498. }
  499. -- Forged Exchange messages
  500. local msgid_dollars_ok = 'Message-Id=/[0-9a-f]{4,}\\$[0-9a-f]{4,}\\$[0-9a-f]{4,}\\@\\S+/H'
  501. local mimeole_ms = 'X-MimeOLE=/^Produced By Microsoft MimeOLE/H'
  502. local rcvd_with_exchange = 'Received=/with Microsoft Exchange Server/H'
  503. reconf['RATWARE_MS_HASH'] = {
  504. re = string.format('(%s) & !(%s) & !(%s)', msgid_dollars_ok, mimeole_ms, rcvd_with_exchange),
  505. score = 2.0,
  506. description = 'Forged Exchange messages',
  507. group = 'headers'
  508. }
  509. -- Reply-type in content-type
  510. reconf['STOX_REPLY_TYPE'] = {
  511. re = 'Content-Type=/text\\/plain; .* reply-type=original/H',
  512. score = 1.0,
  513. description = 'Reply-type in Content-Type header',
  514. group = 'headers'
  515. }
  516. -- Forged yahoo msgid
  517. local at_yahoo_msgid = 'Message-Id=/\\@yahoo\\.com\\b/iH'
  518. local from_yahoo_com = 'From=/\\@yahoo\\.com\\b/iH'
  519. reconf['FORGED_MSGID_YAHOO'] = {
  520. re = string.format('(%s) & !(%s)', at_yahoo_msgid, from_yahoo_com),
  521. score = 2.0,
  522. description = 'Forged Yahoo Message-ID header',
  523. group = 'headers'
  524. }
  525. -- Forged The Bat! MUA headers
  526. local thebat_mua_v1 = 'X-Mailer=/^The Bat! \\(v1\\./H'
  527. local ctype_has_boundary = 'Content-Type=/boundary/iH'
  528. local bat_boundary = 'Content-Type=/boundary=\\"?-{10}/H'
  529. local mailman_21 = 'X-Mailman-Version=/\\d/H'
  530. reconf['FORGED_MUA_THEBAT_BOUN'] = {
  531. re = string.format('(%s) & (%s) & !(%s) & !(%s)', thebat_mua_v1, ctype_has_boundary, bat_boundary, mailman_21),
  532. score = 2.0,
  533. description = 'Forged The Bat! MUA headers',
  534. group = 'headers'
  535. }
  536. -- Detect Mail.Ru web-mail
  537. local xm_mail_ru_mailer_1_0 = 'X-Mailer=/^Mail\\.Ru Mailer 1\\.0$/H'
  538. 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'
  539. reconf['MAIL_RU_MAILER'] = {
  540. re = string.format('(%s) & (%s)', xm_mail_ru_mailer_1_0, rcvd_e_mail_ru),
  541. score = 0.0,
  542. description = 'Sent with Mail.Ru webmail',
  543. group = 'headers'
  544. }
  545. -- Detect yandex.ru web-mail
  546. local xm_yandex_ru_mailer_5_0 = 'X-Mailer=/^Yamail \\[ http:\\/\\/yandex\\.ru \\] 5\\.0$/H'
  547. local rcvd_web_yandex_ru = 'Received=/^by web\\d{1,2}[a-z]\\.yandex\\.ru with HTTP;/mH'
  548. reconf['YANDEX_RU_MAILER'] = {
  549. re = string.format('(%s) & (%s)', xm_yandex_ru_mailer_5_0, rcvd_web_yandex_ru),
  550. score = 0.0,
  551. description = 'Sent with Yandex webmail',
  552. group = 'headers'
  553. }
  554. -- Detect 1C v8.2 and v8.3 mailers
  555. reconf['MAILER_1C_8'] = {
  556. re = 'X-Mailer=/^1C:Enterprise 8\\.[23]$/H',
  557. score = 0.0,
  558. description = 'Sent with 1C:Enterprise 8',
  559. group = 'headers'
  560. }
  561. -- Detect rogue 'strongmail' MTA with IPv4 and '(-)' in Received line
  562. reconf['STRONGMAIL'] = {
  563. re = [[Received=/^from\s+strongmail\s+\(\[\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\]\) by \S+ \(-\); /mH]],
  564. score = 6.0,
  565. description = 'Sent via rogue "strongmail" MTA',
  566. group = 'headers'
  567. }
  568. -- Two received headers with ip addresses
  569. 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'
  570. 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'
  571. reconf['RCVD_DOUBLE_IP_SPAM'] = {
  572. re = string.format('(%s) | (%s)', double_ip_spam_1, double_ip_spam_2),
  573. score = 2.0,
  574. description = 'Has two Received headers containing bare IP addresses',
  575. group = 'headers'
  576. }
  577. -- Quoted reply-to from yahoo (seems to be forged)
  578. local repto_quote = 'Reply-To=/\\".*\\"\\s*\\</H'
  579. reconf['REPTO_QUOTE_YAHOO'] = {
  580. re = string.format('(%s) & ((%s) | (%s))', repto_quote, from_yahoo_com, at_yahoo_msgid),
  581. score = 2.0,
  582. description = 'Quoted Reply-To header from Yahoo (seems to be forged)',
  583. group = 'headers'
  584. }
  585. reconf['FAKE_REPLY'] = {
  586. re = [[Subject=/^re:/i{header} & !(header_exists(In-Reply-To) | header_exists(References))]],
  587. description = 'Fake reply',
  588. score = 1.0,
  589. group = 'headers'
  590. }
  591. -- Mime-OLE is needed but absent (e.g. fake Outlook or fake Exchange)
  592. local has_msmail_pri = 'header_exists(X-MSMail-Priority)'
  593. local has_mimeole = 'header_exists(X-MimeOLE)'
  594. local has_squirrelmail_in_mailer = 'X-Mailer=/SquirrelMail\\b/H'
  595. local has_office_version_in_mailer = [[X-Mailer=/^Microsoft (?:Office )?Outlook [12]\d\.0/]]
  596. local has_x_android_message_id = 'header_exists(X-Android-Message-Id)'
  597. reconf['MISSING_MIMEOLE'] = {
  598. re = string.format('(%s) & !(%s) & !(%s) & !(%s) & !(%s)',
  599. has_msmail_pri,
  600. has_mimeole,
  601. has_squirrelmail_in_mailer,
  602. has_office_version_in_mailer,
  603. has_x_android_message_id),
  604. score = 2.0,
  605. description = 'Mime-OLE is needed but absent (e.g. fake Outlook or fake Exchange)',
  606. group = 'headers'
  607. }
  608. -- Empty delimiters between header names and header values
  609. local function gen_check_header_delimiter_empty(header_name)
  610. return function(task)
  611. for _, rh in ipairs(task:get_header_full(header_name) or {}) do
  612. if rh['empty_separator'] then
  613. return true
  614. end
  615. end
  616. return false
  617. end
  618. end
  619. reconf['HEADER_FROM_EMPTY_DELIMITER'] = {
  620. re = string.format('(%s)', 'lua:check_from_delim_empty'),
  621. score = 1.0,
  622. description = 'From header has no delimiter between header name and header value',
  623. group = 'headers',
  624. functions = {
  625. check_from_delim_empty = gen_check_header_delimiter_empty('From')
  626. }
  627. }
  628. reconf['HEADER_TO_EMPTY_DELIMITER'] = {
  629. re = string.format('(%s)', 'lua:check_to_delim_empty'),
  630. score = 1.0,
  631. description = 'To header has no delimiter between header name and header value',
  632. group = 'headers',
  633. functions = {
  634. check_to_delim_empty = gen_check_header_delimiter_empty('To')
  635. }
  636. }
  637. reconf['HEADER_CC_EMPTY_DELIMITER'] = {
  638. re = string.format('(%s)', 'lua:check_cc_delim_empty'),
  639. score = 1.0,
  640. description = 'Cc header has no delimiter between header name and header value',
  641. group = 'headers',
  642. functions = {
  643. check_cc_delim_empty = gen_check_header_delimiter_empty('Cc')
  644. }
  645. }
  646. reconf['HEADER_REPLYTO_EMPTY_DELIMITER'] = {
  647. re = string.format('(%s)', 'lua:check_repto_delim_empty'),
  648. score = 1.0,
  649. description = 'Reply-To header has no delimiter between header name and header value',
  650. group = 'headers',
  651. functions = {
  652. check_repto_delim_empty = gen_check_header_delimiter_empty('Reply-To')
  653. }
  654. }
  655. reconf['HEADER_DATE_EMPTY_DELIMITER'] = {
  656. re = string.format('(%s)', 'lua:check_date_delim_empty'),
  657. score = 1.0,
  658. description = 'Date header has no delimiter between header name and header value',
  659. group = 'headers',
  660. functions = {
  661. check_date_delim_empty = gen_check_header_delimiter_empty('Date')
  662. }
  663. }
  664. -- Definitions of received headers regexp
  665. reconf['RCVD_ILLEGAL_CHARS'] = {
  666. re = 'Received=/[\\x80-\\xff]/X',
  667. score = 4.0,
  668. description = 'Received header has raw illegal character',
  669. group = 'headers'
  670. }
  671. local MAIL_RU_Return_Path = 'Return-path=/^\\s*<.+\\@mail\\.ru>$/iX'
  672. local MAIL_RU_X_Envelope_From = 'X-Envelope-From=/^\\s*<.+\\@mail\\.ru>$/iX'
  673. local MAIL_RU_From = 'From=/\\@mail\\.ru>?$/iX'
  674. local MAIL_RU_Received = 'Received=/from mail\\.ru \\(/mH'
  675. reconf['FAKE_RECEIVED_mail_ru'] = {
  676. re = string.format('(%s) & !(((%s) | (%s)) & (%s))',
  677. MAIL_RU_Received, MAIL_RU_Return_Path, MAIL_RU_X_Envelope_From, MAIL_RU_From),
  678. score = 4.0,
  679. description = 'Fake HELO mail.ru in Received header from non-mail.ru sender address',
  680. group = 'headers'
  681. }
  682. local GMAIL_COM_Return_Path = 'Return-path=/^\\s*<.+\\@gmail\\.com>$/iX'
  683. local GMAIL_COM_X_Envelope_From = 'X-Envelope-From=/^\\s*<.+\\@gmail\\.com>$/iX'
  684. local GMAIL_COM_From = 'From=/\\@gmail\\.com>?$/iX'
  685. local UKR_NET_Return_Path = 'Return-path=/^\\s*<.+\\@ukr\\.net>$/iX'
  686. local UKR_NET_X_Envelope_From = 'X-Envelope-From=/^\\s*<.+\\@ukr\\.net>$/iX'
  687. local UKR_NET_From = 'From=/\\@ukr\\.net>?$/iX'
  688. local RECEIVED_smtp_yandex_ru_1 = 'Received=/from \\[\\d+\\.\\d+\\.\\d+\\.\\d+\\] \\((port=\\d+ )?helo=smtp\\.yandex\\.ru\\)/iX'
  689. local RECEIVED_smtp_yandex_ru_2 = 'Received=/from \\[UNAVAILABLE\\] \\(\\[\\d+\\.\\d+\\.\\d+\\.\\d+\\]:\\d+ helo=smtp\\.yandex\\.ru\\)/iX'
  690. local RECEIVED_smtp_yandex_ru_3 = 'Received=/from \\S+ \\(\\[\\d+\\.\\d+\\.\\d+\\.\\d+\\]:\\d+ helo=smtp\\.yandex\\.ru\\)/iX'
  691. local RECEIVED_smtp_yandex_ru_4 = 'Received=/from \\[\\d+\\.\\d+\\.\\d+\\.\\d+\\] \\(account \\S+ HELO smtp\\.yandex\\.ru\\)/iX'
  692. local RECEIVED_smtp_yandex_ru_5 = 'Received=/from smtp\\.yandex\\.ru \\(\\[\\d+\\.\\d+\\.\\d+\\.\\d+\\]\\)/iX'
  693. local RECEIVED_smtp_yandex_ru_6 = 'Received=/from smtp\\.yandex\\.ru \\(\\S+ \\[\\d+\\.\\d+\\.\\d+\\.\\d+\\]\\)/iX'
  694. local RECEIVED_smtp_yandex_ru_7 = 'Received=/from \\S+ \\(HELO smtp\\.yandex\\.ru\\) \\(\\S+\\@\\d+\\.\\d+\\.\\d+\\.\\d+\\)/iX'
  695. local RECEIVED_smtp_yandex_ru_8 = 'Received=/from \\S+ \\(HELO smtp\\.yandex\\.ru\\) \\(\\d+\\.\\d+\\.\\d+\\.\\d+\\)/iX'
  696. local RECEIVED_smtp_yandex_ru_9 = 'Received=/from \\S+ \\(\\[\\d+\\.\\d+\\.\\d+\\.\\d+\\] helo=smtp\\.yandex\\.ru\\)/iX'
  697. reconf['FAKE_RECEIVED_smtp_yandex_ru'] = {
  698. re = string.format('(((%s) & ((%s) | (%s))) | ((%s) & ((%s) | (%s))) ' ..
  699. ' | ((%s) & ((%s) | (%s)))) & (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s)',
  700. MAIL_RU_From, MAIL_RU_Return_Path, MAIL_RU_X_Envelope_From, GMAIL_COM_From,
  701. GMAIL_COM_Return_Path, GMAIL_COM_X_Envelope_From, UKR_NET_From, UKR_NET_Return_Path,
  702. UKR_NET_X_Envelope_From, RECEIVED_smtp_yandex_ru_1, RECEIVED_smtp_yandex_ru_2,
  703. RECEIVED_smtp_yandex_ru_3, RECEIVED_smtp_yandex_ru_4, RECEIVED_smtp_yandex_ru_5,
  704. RECEIVED_smtp_yandex_ru_6, RECEIVED_smtp_yandex_ru_7, RECEIVED_smtp_yandex_ru_8,
  705. RECEIVED_smtp_yandex_ru_9),
  706. score = 4.0,
  707. description = 'Fake smtp.yandex.ru Received header',
  708. group = 'headers'
  709. }
  710. reconf['FORGED_GENERIC_RECEIVED'] = {
  711. 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',
  712. score = 3.6,
  713. description = 'Forged generic Received header',
  714. group = 'headers'
  715. }
  716. reconf['FORGED_GENERIC_RECEIVED2'] = {
  717. 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',
  718. score = 3.6,
  719. description = 'Forged generic Received header',
  720. group = 'headers'
  721. }
  722. reconf['FORGED_GENERIC_RECEIVED3'] = {
  723. 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',
  724. score = 3.6,
  725. description = 'Forged generic Received header',
  726. group = 'headers'
  727. }
  728. reconf['FORGED_GENERIC_RECEIVED4'] = {
  729. 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',
  730. score = 3.6,
  731. description = 'Forged generic Received header',
  732. group = 'headers'
  733. }
  734. reconf['INVALID_POSTFIX_RECEIVED'] = {
  735. 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',
  736. score = 3.0,
  737. description = 'Invalid Postfix Received header',
  738. group = 'headers'
  739. }
  740. reconf['RECEIVED_HELO_LOCALHOST'] = {
  741. re = 'Received=/from (localhost|\\[127\\.0\\.0\\.1\\])\\s+/X',
  742. score = 0.0,
  743. description = 'Localhost HELO seen in Received header',
  744. group = 'headers'
  745. }
  746. reconf['X_PHP_FORGED_0X'] = {
  747. re = "X-PHP-Originating-Script=/^0\\d/X",
  748. score = 4.0,
  749. description = "X-PHP-Originating-Script header appears forged",
  750. group = 'headers'
  751. }
  752. reconf['GOOGLE_FORWARDING_MID_MISSING'] = {
  753. re = "Message-ID=/SMTPIN_ADDED_MISSING\\@mx\\.google\\.com>$/X",
  754. score = 2.5,
  755. description = "Message was missing Message-ID pre-forwarding",
  756. group = 'headers'
  757. }
  758. reconf['GOOGLE_FORWARDING_MID_BROKEN'] = {
  759. re = "Message-ID=/SMTPIN_ADDED_BROKEN\\@mx\\.google\\.com>$/X",
  760. score = 1.7,
  761. description = "Message had invalid Message-ID pre-forwarding",
  762. group = 'headers'
  763. }
  764. reconf['CTE_CASE'] = {
  765. re = 'Content-Transfer-Encoding=/^[78]B/X',
  766. description = '[78]Bit .vs. [78]bit',
  767. score = 0.5,
  768. group = 'headers'
  769. }
  770. reconf['HAS_INTERSPIRE_SIG'] = {
  771. re = string.format('((%s) & (%s) & (%s) & (%s)) | (%s)',
  772. 'header_exists(X-Mailer-LID)',
  773. 'header_exists(X-Mailer-RecptId)',
  774. 'header_exists(X-Mailer-SID)',
  775. 'header_exists(X-Mailer-Sent-By)',
  776. 'List-Unsubscribe=/\\/unsubscribe\\.php\\?M=[^&]+&C=[^&]+&L=[^&]+&N=[^>]+>$/Xi'),
  777. description = "Has Interspire fingerprint",
  778. score = 1.0,
  779. group = 'headers'
  780. }
  781. reconf['CT_EXTRA_SEMI'] = {
  782. re = 'Content-Type=/;$/X',
  783. description = 'Content-Type header ends with a semi-colon',
  784. score = 1.0,
  785. group = 'headers'
  786. }
  787. reconf['SUBJECT_ENDS_EXCLAIM'] = {
  788. re = 'Subject=/!\\s*$/H',
  789. description = 'Subject ends with an exclamation mark',
  790. score = 0.0,
  791. group = 'headers'
  792. }
  793. reconf['SUBJECT_HAS_EXCLAIM'] = {
  794. re = string.format('%s & !%s', 'Subject=/!/H', 'Subject=/!\\s*$/H'),
  795. description = 'Subject contains an exclamation mark',
  796. score = 0.0,
  797. group = 'headers'
  798. }
  799. reconf['SUBJECT_ENDS_QUESTION'] = {
  800. re = 'Subject=/\\?\\s*$/Hu',
  801. description = 'Subject ends with a question mark',
  802. score = 1.0,
  803. group = 'headers'
  804. }
  805. reconf['SUBJECT_HAS_QUESTION'] = {
  806. re = string.format('%s & !%s', 'Subject=/\\?/H', 'Subject=/\\?\\s*$/Hu'),
  807. description = 'Subject contains a question mark',
  808. score = 0.0,
  809. group = 'headers'
  810. }
  811. reconf['SUBJECT_HAS_CURRENCY'] = {
  812. re = 'Subject=/\\p{Sc}/Hu',
  813. description = 'Subject contains currency',
  814. score = 1.0,
  815. group = 'headers'
  816. }
  817. reconf['SUBJECT_ENDS_SPACES'] = {
  818. re = 'Subject=/\\s+$/H',
  819. description = 'Subject ends with space characters',
  820. score = 0.5,
  821. group = 'headers'
  822. }
  823. reconf['HAS_ORG_HEADER'] = {
  824. re = string.format('%s || %s', 'header_exists(Organization)', 'header_exists(Organisation)'),
  825. description = 'Has Organization header',
  826. score = 0.0,
  827. group = 'headers'
  828. }
  829. reconf['HAS_CD_HEADER'] = {
  830. re = 'header_exists(Content-Description)',
  831. description = 'Has Content-Description header',
  832. score = 0.0,
  833. group = 'headers'
  834. }
  835. reconf['CD_MM_BODY'] = {
  836. re = 'Content-Description=/^Mail message body$/Hi',
  837. description = 'Content-Description header reads "Mail message body", commonly seen in spam',
  838. score = 2.0,
  839. group = 'headers'
  840. }
  841. reconf['X_PHPOS_FAKE'] = {
  842. re = 'X-PHP-Originating-Script=/^\\d{7}:/Hi',
  843. description = 'Fake X-PHP-Originating-Script header',
  844. score = 3.0,
  845. group = 'headers'
  846. }
  847. reconf['HAS_XOIP'] = {
  848. re = "header_exists('X-Originating-IP')",
  849. description = "Has X-Originating-IP header",
  850. score = 0.0,
  851. group = 'headers'
  852. }
  853. reconf['HAS_LIST_UNSUB'] = {
  854. re = string.format('%s', 'header_exists(List-Unsubscribe)'),
  855. description = 'Has List-Unsubscribe header',
  856. score = -0.01,
  857. group = 'headers'
  858. }
  859. reconf['HAS_GUC_PROXY_URI'] = {
  860. re = '/\\.googleusercontent\\.com\\/proxy/{url}i',
  861. description = 'Has googleusercontent.com proxy URL',
  862. score = 1.0,
  863. group = 'url'
  864. }
  865. reconf['HAS_GOOGLE_REDIR'] = {
  866. re = '/\\.google\\.([a-z]{2,3}(|\\.[a-z]{2,3})|info|jobs)\\/(amp\\/s\\/|url\\?)/{url}i',
  867. description = 'Has google.com/url or alike Google redirection URL',
  868. score = 1.0,
  869. group = 'url'
  870. }
  871. reconf['HAS_GOOGLE_FIREBASE_URL'] = {
  872. re = '/\\.firebasestorage\\.googleapis\\.com\\//{url}i',
  873. description = 'Contains firebasestorage.googleapis.com URL',
  874. score = 2.0,
  875. group = 'url'
  876. }
  877. reconf['HAS_FILE_URL'] = {
  878. re = '/^file:\\/\\//{url}i',
  879. description = 'Contains file:// URL',
  880. score = 2.0,
  881. group = 'url'
  882. }
  883. reconf['XM_UA_NO_VERSION'] = {
  884. re = string.format('(!%s && !%s) && (%s || %s)',
  885. 'X-Mailer=/https?:/H',
  886. 'User-Agent=/https?:/H',
  887. 'X-Mailer=/^[^0-9]+$/H',
  888. 'User-Agent=/^[^0-9]+$/H'),
  889. description = 'X-Mailer/User-Agent header has no version number',
  890. score = 0.01,
  891. group = 'experimental'
  892. }
  893. -- Detects messages missing both X-Mailer and User-Agent header
  894. local has_ua = 'header_exists(User-Agent)'
  895. local has_xmailer = 'header_exists(X-Mailer)'
  896. reconf['MISSING_XM_UA'] = {
  897. re = string.format('!%s && !%s', has_xmailer, has_ua),
  898. score = 0.0,
  899. description = 'Message has neither X-Mailer nor User-Agent header',
  900. group = 'headers',
  901. }
  902. -- X-Mailer for old MUA versions which are forged by spammers
  903. local old_x_mailers = {
  904. -- Outlook Express 6.0 was last included in Windows XP (EOL 2014). Windows
  905. -- XP is still used (in 2020) by relatively small number of internet users,
  906. -- but this header is widely abused by spammers.
  907. 'Microsoft Outlook Express',
  908. -- Qualcomm Eudora for Windows 7.1.0.9 was released in 2006
  909. [[QUALCOMM Windows Eudora (Pro )?Version [1-6]\.]],
  910. -- The Bat 3.0 was released in 2004
  911. [[The Bat! \(v[12]\.]],
  912. -- Can be found in public maillist archives, messages circa 2000
  913. [[Microsoft Outlook IMO, Build 9\.0\.]],
  914. -- Outlook 2002 (Office XP)
  915. [[Microsoft Outlook, Build 10\.]],
  916. -- Some old Apple iOS versions are used on old devices, match only very old
  917. -- versions (iOS 4.3.5 buid 8L1 was supported until 2013) and less old
  918. -- versions frequently seen in spam
  919. [[i(Phone|Pad) Mail \((?:[1-8][A-L]|12H|13E)]],
  920. }
  921. reconf['OLD_X_MAILER'] = {
  922. description = 'X-Mailer header has a very old MUA version',
  923. re = string.format('X-Mailer=/^(?:%s)/{header}', table.concat(old_x_mailers, '|')),
  924. score = 2.0,
  925. group = 'headers',
  926. }
  927. -- Detect Apple Mail
  928. local apple_x_mailer = [[Apple Mail \((?:(?:Version )?[1-9]\d{0,2}\.\d{1,3}|[1-9]\d{0,2}\.\d{1,4}\.\d{1,4}\.\d{1,4})\)]]
  929. reconf['APPLE_MAILER'] = {
  930. description = 'Sent with Apple Mail',
  931. re = string.format('X-Mailer=/^%s/{header}', apple_x_mailer),
  932. score = 0.0,
  933. group = 'headers',
  934. }
  935. -- Detect Apple iPhone/iPad Mail
  936. -- Apple iPhone/iPad Mail X-Mailer contains iOS build number, e. g. 9B206, 16H5, 18G5023c
  937. -- https://en.wikipedia.org/wiki/IOS_version_history
  938. local apple_ios_x_mailer = [[i(?:Phone|Pad) Mail \(\d{1,2}[A-Z]\d{1,4}[a-z]?\)]]
  939. reconf['APPLE_IOS_MAILER'] = {
  940. description = 'Sent with Apple iPhone/iPad Mail',
  941. re = string.format('X-Mailer=/^%s/{header}', apple_ios_x_mailer),
  942. score = 0.0,
  943. group = 'headers',
  944. }
  945. -- X-Mailer header values which should not occur (in the modern mail) at all
  946. local bad_x_mailers = {
  947. -- header name repeated in the header value
  948. [[X-Mailer: ]],
  949. -- Mozilla Thunderbird uses User-Agent header, not X-Mailer
  950. -- Early Thunderbird had U-A like:
  951. -- Mozilla Thunderbird 1.0.2 (Windows/20050317)
  952. -- Thunderbird 2.0.0.23 (X11/20090812)
  953. [[(?:Mozilla )?Thunderbird \d]],
  954. -- Was used by Yahoo Groups in 2000s, no one expected to use this in 2020s
  955. [[eGroups Message Poster]],
  956. -- Regexp for genuine iOS X-Mailer is below, anything which doesn't match it,
  957. -- but starts with 'iPhone Mail' or 'iPad Mail' is likely fake
  958. [[i(?:Phone|Pad) Mail]],
  959. }
  960. reconf['FORGED_X_MAILER'] = {
  961. description = 'Forged X-Mailer header',
  962. re = string.format('X-Mailer=/^(?:%s)/{header} && !X-Mailer=/^%s/{header}',
  963. table.concat(bad_x_mailers, '|'), apple_ios_x_mailer),
  964. score = 4.5,
  965. group = 'headers',
  966. }
  967. -- X-Mailer headers like: 'Internet Mail Service (5.5.2650.21)' are being
  968. -- forged by spammers, but MS Exchange 5.5 is still being used (in 2020) on
  969. -- some mail servers. Example of genuine headers (DC-EXMPL is a hostname which
  970. -- can be a FQDN):
  971. -- Received: by DC-EXMPL with Internet Mail Service (5.5.2656.59)
  972. -- id <HKH4BJQX>; Tue, 8 Dec 2020 07:10:54 -0600
  973. -- Message-ID: <E7209F9DB64FCC4BB1051420F0E955DD05C9D59F@DC-EXMPL>
  974. -- X-Mailer: Internet Mail Service (5.5.2656.59)
  975. reconf['FORGED_IMS'] = {
  976. description = 'Forged X-Mailer: Internet Mail Service',
  977. re = [[X-Mailer=/^Internet Mail Service \(5\./{header} & !Received=/^by \S+ with Internet Mail Service \(5\./{header}]],
  978. score = 3.0,
  979. group = 'headers',
  980. }