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

headers.lua 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. -- Actually these regular expressions were obtained from SpamAssassin project, so they are licensed by apache license:
  2. --
  3. -- Licensed to the Apache Software Foundation (ASF) under one or more
  4. -- contributor license agreements. See the NOTICE file distributed with
  5. -- this work for additional information regarding copyright ownership.
  6. -- The ASF licenses this file to you under the Apache License, Version 2.0
  7. -- (the "License"); you may not use this file except in compliance with
  8. -- the License. You may obtain a copy of the License at:
  9. --
  10. -- http://www.apache.org/licenses/LICENSE-2.0
  11. --
  12. -- Unless required by applicable law or agreed to in writing, software
  13. -- distributed under the License is distributed on an "AS IS" BASIS,
  14. -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. -- See the License for the specific language governing permissions and
  16. -- limitations under the License.
  17. --
  18. -- Definitions of header regexps
  19. local reconf = config['regexp']
  20. local rspamd_regexp = require "rspamd_regexp"
  21. -- Subject needs encoding
  22. -- Define encodings types
  23. local subject_encoded_b64 = 'Subject=/=\\?\\S+\\?B\\?/iX'
  24. local subject_encoded_qp = 'Subject=/=\\?\\S+\\?Q\\?/iX'
  25. -- Define whether subject must be encoded (contains non-7bit characters)
  26. local subject_needs_mime = 'Subject=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/X'
  27. -- Final rule
  28. reconf['SUBJECT_NEEDS_ENCODING'] = string.format('!(%s) & !(%s) & (%s)', subject_encoded_b64, subject_encoded_qp, subject_needs_mime)
  29. -- Detects that there is no space in From header (e.g. Some Name<some@host>)
  30. reconf['R_NO_SPACE_IN_FROM'] = 'From=/\\S<[-\\w\\.]+\\@[-\\w\\.]+>/X'
  31. rspamd_config.MISSING_SUBJECT = function(task)
  32. local hdr = task:get_header('Subject')
  33. if not hdr or #hdr == 0 then
  34. return true
  35. end
  36. return false
  37. end
  38. -- Detects bad content-transfer-encoding for text parts
  39. -- For text parts (text/plain and text/html mainly)
  40. local r_ctype_text = 'content_type_is_type(text)'
  41. -- Content transfer encoding is 7bit
  42. local r_cte_7bit = 'compare_transfer_encoding(7bit)'
  43. -- And body contains 8bit characters
  44. local r_body_8bit = '/[^\\x01-\\x7f]/Pr'
  45. reconf['R_BAD_CTE_7BIT'] = string.format('(%s) & (%s) & (%s)', r_ctype_text, r_cte_7bit, r_body_8bit)
  46. -- Detects missing To header
  47. reconf['MISSING_TO']= '!raw_header_exists(To)';
  48. -- Detects undisclosed recipients
  49. local undisc_rcpt = 'To=/^<?undisclosed[- ]recipient/Hi'
  50. reconf['R_UNDISC_RCPT'] = string.format('(%s)', undisc_rcpt)
  51. -- Detects missing Message-Id
  52. local has_mid = 'header_exists(Message-Id)'
  53. reconf['MISSING_MID'] = '!header_exists(Message-Id)';
  54. -- Received seems to be fake
  55. reconf['R_RCVD_SPAMBOTS'] = 'Received=/^from \\[\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\] by [-.\\w+]{5,255}; [SMTWF][a-z][a-z], [\\s\\d]?\\d [JFMAJSOND][a-z][a-z] \\d{4} \\d{2}:\\d{2}:\\d{2} [-+]\\d{4}$/mH'
  56. -- Charset is missing in message
  57. reconf['R_MISSING_CHARSET']= string.format('content_type_is_type(text) & !content_type_has_param(charset) & !%s', r_cte_7bit);
  58. -- Subject seems to be spam
  59. reconf['R_SAJDING'] = 'Subject=/\\bsajding(?:om|a)?\\b/iH'
  60. -- Find forged Outlook MUA
  61. -- Yahoo groups messages
  62. local yahoo_bulk = 'Received=/from \\[\\S+\\] by \\S+\\.(?:groups|scd|dcn)\\.yahoo\\.com with NNFMP/H'
  63. -- Outlook MUA
  64. local outlook_mua = 'X-Mailer=/^Microsoft Outlook\\b/H'
  65. local any_outlook_mua = 'X-Mailer=/^Microsoft Outlook\\b/H'
  66. reconf['FORGED_OUTLOOK_HTML'] = string.format('!%s & %s & %s', yahoo_bulk, outlook_mua, 'has_only_html_part()')
  67. -- Recipients seems to be likely with each other (only works when recipients count is more than 5 recipients)
  68. reconf['SUSPICIOUS_RECIPS'] = 'compare_recipients_distance(0.65)'
  69. -- Recipients list seems to be sorted
  70. reconf['SORTED_RECIPS'] = 'is_recipients_sorted()'
  71. -- Spam string at the end of message to make statistics faults
  72. reconf['TRACKER_ID'] = '/^[a-z0-9]{6,24}[-_a-z0-9]{12,36}[a-z0-9]{6,24}\\s*\\z/isPr'
  73. -- From that contains encoded characters while base 64 is not needed as all symbols are 7bit
  74. -- Regexp that checks that From header is encoded with base64 (search in raw headers)
  75. local from_encoded_b64 = 'From=/\\=\\?\\S+\\?B\\?/iX'
  76. -- From contains only 7bit characters (parsed headers are used)
  77. local from_needs_mime = 'From=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/Hr'
  78. -- Final rule
  79. reconf['FROM_EXCESS_BASE64'] = string.format('%s & !%s', from_encoded_b64, from_needs_mime)
  80. -- From that contains encoded characters while quoted-printable is not needed as all symbols are 7bit
  81. -- Regexp that checks that From header is encoded with quoted-printable (search in raw headers)
  82. local from_encoded_qp = 'From=/\\=\\?\\S+\\?Q\\?/iX'
  83. -- Final rule
  84. reconf['FROM_EXCESS_QP'] = string.format('%s & !%s', from_encoded_qp, from_needs_mime)
  85. -- To that contains encoded characters while base 64 is not needed as all symbols are 7bit
  86. -- Regexp that checks that To header is encoded with base64 (search in raw headers)
  87. local to_encoded_b64 = 'To=/\\=\\?\\S+\\?B\\?/iX'
  88. -- To contains only 7bit characters (parsed headers are used)
  89. local to_needs_mime = 'To=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/Hr'
  90. -- Final rule
  91. reconf['TO_EXCESS_BASE64'] = string.format('%s & !%s', to_encoded_b64, to_needs_mime)
  92. -- To that contains encoded characters while quoted-printable is not needed as all symbols are 7bit
  93. -- Regexp that checks that To header is encoded with quoted-printable (search in raw headers)
  94. local to_encoded_qp = 'To=/\\=\\?\\S+\\?Q\\?/iX'
  95. -- Final rule
  96. reconf['TO_EXCESS_QP'] = string.format('%s & !%s', to_encoded_qp, to_needs_mime)
  97. -- Reply-To that contains encoded characters while base 64 is not needed as all symbols are 7bit
  98. -- Regexp that checks that Reply-To header is encoded with base64 (search in raw headers)
  99. local replyto_encoded_b64 = 'Reply-To=/\\=\\?\\S+\\?B\\?/iX'
  100. -- Reply-To contains only 7bit characters (parsed headers are used)
  101. local replyto_needs_mime = 'Reply-To=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/Hr'
  102. -- Final rule
  103. reconf['REPLYTO_EXCESS_BASE64'] = string.format('%s & !%s', replyto_encoded_b64, replyto_needs_mime)
  104. -- Reply-To that contains encoded characters while quoted-printable is not needed as all symbols are 7bit
  105. -- Regexp that checks that Reply-To header is encoded with quoted-printable (search in raw headers)
  106. local replyto_encoded_qp = 'Reply-To=/\\=\\?\\S+\\?Q\\?/iX'
  107. -- Final rule
  108. reconf['REPLYTO_EXCESS_QP'] = string.format('%s & !%s', replyto_encoded_qp, replyto_needs_mime)
  109. -- Cc that contains encoded characters while base 64 is not needed as all symbols are 7bit
  110. -- Regexp that checks that Cc header is encoded with base64 (search in raw headers)
  111. local cc_encoded_b64 = 'Cc=/\\=\\?\\S+\\?B\\?/iX'
  112. -- Co contains only 7bit characters (parsed headers are used)
  113. local cc_needs_mime = 'Cc=/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\xff]/Hr'
  114. -- Final rule
  115. reconf['CC_EXCESS_BASE64'] = string.format('%s & !%s', cc_encoded_b64, cc_needs_mime)
  116. -- Cc that contains encoded characters while quoted-printable is not needed as all symbols are 7bit
  117. -- Regexp that checks that Cc header is encoded with quoted-printable (search in raw headers)
  118. local cc_encoded_qp = 'Cc=/\\=\\?\\S+\\?Q\\?/iX'
  119. -- Final rule
  120. reconf['CC_EXCESS_QP'] = string.format('%s & !%s', cc_encoded_qp, cc_needs_mime)
  121. -- Detect forged outlook headers
  122. -- OE X-Mailer header
  123. local oe_mua = 'X-Mailer=/\\bOutlook Express [456]\\./H'
  124. -- OE Message ID format
  125. local oe_msgid_1 = 'Message-Id=/^<?[A-Za-z0-9-]{7}[A-Za-z0-9]{20}\\@hotmail\\.com>?$/mH'
  126. local oe_msgid_2 = 'Message-Id=/^<?(?:[0-9a-f]{8}|[0-9a-f]{12})\\$[0-9a-f]{8}\\$[0-9a-f]{8}\\@\\S+>?$/H'
  127. -- EZLM remail of message
  128. local lyris_ezml_remailer = 'List-Unsubscribe=/<mailto:(?:leave-\\S+|\\S+-unsubscribe)\\@\\S+>$/H'
  129. -- Header of wacky sendmail
  130. local wacky_sendmail_version = 'Received=/\\/CWT\\/DCE\\)/H'
  131. -- Iplanet received header
  132. local iplanet_messaging_server = 'Received=/iPlanet Messaging Server/H'
  133. -- Hotmail message id
  134. local hotmail_baydav_msgid = 'Message-Id=/^<?BAY\\d+-DAV\\d+[A-Z0-9]{25}\\@phx\\.gbl?>$/H'
  135. -- Sympatico message id
  136. local sympatico_msgid = 'Message-Id=/^<?BAYC\\d+-PASMTP\\d+[A-Z0-9]{25}\\@CEZ\\.ICE>?$/H'
  137. -- Mailman message id
  138. local mailman_msgid = 'Message-ID=/^<mailman\\.\\d+\\.\\d+\\.\\d+\\..+\\@\\S+>$/H'
  139. -- Message id seems to be forged
  140. local unusable_msgid = string.format('(%s | %s | %s | %s | %s | %s)',
  141. lyris_ezml_remailer, wacky_sendmail_version, iplanet_messaging_server, hotmail_baydav_msgid, sympatico_msgid, mailman_msgid)
  142. -- Outlook express data seems to be forged
  143. local forged_oe = string.format('(%s & !%s & !%s & !%s)', oe_mua, oe_msgid_1, oe_msgid_2, unusable_msgid)
  144. -- Outlook specific headers
  145. local outlook_dollars_mua = 'X-Mailer=/^Microsoft Outlook(?: 8| CWS, Build 9|, Build 10)\\./H'
  146. local outlook_dollars_other = 'Message-Id=/^<?\\!\\~\\!>?/H'
  147. local vista_msgid = 'Message-Id=/^<?[A-F\\d]{32}\\@\\S+>?$/H'
  148. local ims_msgid = 'Message-Id=/^<?[A-F\\d]{36,40}\\@\\S+>?$/H'
  149. -- Forged outlook headers
  150. local forged_outlook_dollars = string.format('(%s & !%s & !%s & !%s & !%s & !%s)',
  151. outlook_dollars_mua, oe_msgid_2, outlook_dollars_other, vista_msgid, ims_msgid, unusable_msgid)
  152. -- Outlook versions that should be excluded from summary rule
  153. local fmo_excl_o3416 = 'X-Mailer=/^Microsoft Outlook, Build 10.0.3416$/H'
  154. local fmo_excl_oe3790 = 'X-Mailer=/^Microsoft Outlook Express 6.00.3790.3959$/H'
  155. -- Summary rule for forged outlook
  156. reconf['FORGED_MUA_OUTLOOK'] = string.format('(%s | %s) & !%s & !%s & !%s',
  157. forged_oe, forged_outlook_dollars, fmo_excl_o3416, fmo_excl_oe3790, vista_msgid)
  158. -- HTML outlook signs
  159. local mime_html = 'content_type_is_type(text) & content_type_is_subtype(/.?html/)'
  160. local tag_exists_html = 'has_html_tag(html)'
  161. local tag_exists_head = 'has_html_tag(head)'
  162. local tag_exists_meta = 'has_html_tag(meta)'
  163. local tag_exists_body = 'has_html_tag(body)'
  164. reconf['FORGED_OUTLOOK_TAGS'] = string.format('!%s & %s & %s & !(%s & %s & %s & %s)',
  165. yahoo_bulk, any_outlook_mua, mime_html, tag_exists_html, tag_exists_head,
  166. tag_exists_meta, tag_exists_body)
  167. -- Forged OE/MSO boundary
  168. reconf['SUSPICIOUS_BOUNDARY'] = 'Content-Type=/^\\s*multipart.+boundary="----=_NextPart_000_[A-Z\\d]{4}_(00EBFFA4|0102FFA4|32C6FFA4|3302FFA4)\\.[A-Z\\d]{8}"[\\r\\n]*$/siX'
  169. -- Forged OE/MSO boundary
  170. reconf['SUSPICIOUS_BOUNDARY2'] = 'Content-Type=/^\\s*multipart.+boundary="----=_NextPart_000_[A-Z\\d]{4}_(01C6527E)\\.[A-Z\\d]{8}"[\\r\\n]*$/siX'
  171. -- Forged OE/MSO boundary
  172. reconf['SUSPICIOUS_BOUNDARY3'] = 'Content-Type=/^\\s*multipart.+boundary="-----000-00\\d\\d-01C[\\dA-F]{5}-[\\dA-F]{8}"[\\r\\n]*$/siX'
  173. -- Forged OE/MSO boundary
  174. 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'
  175. local suspicious_boundary_01C4_date = 'Date=/^\\s*\\w\\w\\w,\\s+\\d+\\s+\\w\\w\\w 20(0[56789]|1\\d)/'
  176. reconf['SUSPICIOUS_BOUNDARY4'] = string.format('(%s) & (%s)', suspicious_boundary_01C4, suspicious_boundary_01C4_date)
  177. -- Detect forged The Bat! headers
  178. -- The Bat! X-Mailer header
  179. local thebat_mua_any = 'X-Mailer=/^\\s*The Bat!/H'
  180. -- The Bat! common Message-ID template
  181. local thebat_msgid_common = 'Message-ID=/^<?\\d+\\.\\d+\\@\\S+>?$/mH'
  182. -- Correct The Bat! Message-ID template
  183. 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'
  184. -- Summary rule for forged The Bat! Message-ID header
  185. reconf['FORGED_MUA_THEBAT_MSGID'] = string.format('(%s) & !(%s) & (%s) & !(%s)', thebat_mua_any, thebat_msgid, thebat_msgid_common, unusable_msgid)
  186. -- Summary rule for forged The Bat! Message-ID header with unknown template
  187. reconf['FORGED_MUA_THEBAT_MSGID_UNKNOWN'] = string.format('(%s) & !(%s) & !(%s) & !(%s)', thebat_mua_any, thebat_msgid, thebat_msgid_common, unusable_msgid)
  188. -- Detect forged KMail headers
  189. -- KMail User-Agent header
  190. local kmail_mua = 'User-Agent=/^\\s*KMail\\/1\\.\\d+\\.\\d+/H'
  191. -- KMail common Message-ID template
  192. local kmail_msgid_common = 'Message-Id=/^<?\\s*\\d+\\.\\d+\\.\\S+\\@\\S+>?$/mH'
  193. function kmail_msgid (task)
  194. local regexp_text = '<(\\S+)>\\|(19[789]\\d|20\\d\\d)(0\\d|1[012])([012]\\d|3[01])([0-5]\\d)([0-5]\\d)\\.\\d+\\.\\1$'
  195. local re = rspamd_regexp.create_cached(regexp_text)
  196. local header_msgid = task:get_header('Message-Id')
  197. if header_msgid then
  198. local header_from = task:get_header('From')
  199. if header_from and re:match(header_from.."|"..header_msgid) then return true end
  200. end
  201. return false
  202. end
  203. -- Summary rule for forged KMail Message-ID header
  204. reconf['FORGED_MUA_KMAIL_MSGID'] = string.format('(%s) & (%s) & !(%s) & !(%s)', kmail_mua, kmail_msgid_common, 'kmail_msgid', unusable_msgid)
  205. -- Summary rule for forged KMail Message-ID header with unknown template
  206. reconf['FORGED_MUA_KMAIL_MSGID_UNKNOWN'] = string.format('(%s) & !(%s) & !(%s)', kmail_mua, kmail_msgid_common, unusable_msgid)
  207. -- Detect forged Opera Mail headers
  208. -- Opera Mail User-Agent header
  209. local opera1x_mua = 'User-Agent=/^\\s*Opera Mail\\/1[01]\\.\\d+ /H'
  210. -- Opera Mail Message-ID template
  211. local opera1x_msgid = 'Message-ID=/^<?op\\.[a-z\\d]{14}\\@\\S+>?$/H'
  212. -- Suspicious Opera Mail User-Agent header
  213. local suspicious_opera10w_mua = 'User-Agent=/^\\s*Opera Mail\\/10\\.\\d+ \\(Windows\\)$/H'
  214. -- Suspicious Opera Mail Message-ID, apparently from KMail
  215. local suspicious_opera10w_msgid = 'Message-Id=/^<?2009\\d{8}\\.\\d+\\.\\S+\\@\\S+?>$/H'
  216. -- Summary rule for forged Opera Mail User-Agent header and Message-ID header from KMail
  217. reconf['SUSPICIOUS_OPERA_10W_MSGID'] = string.format('(%s) & (%s)', suspicious_opera10w_mua, suspicious_opera10w_msgid)
  218. -- Summary rule for forged Opera Mail Message-ID header
  219. reconf['FORGED_MUA_OPERA_MSGID'] = string.format('(%s) & !(%s) & !(%s) & !(%s)', opera1x_mua, opera1x_msgid, reconf['SUSPICIOUS_OPERA_10W_MSGID'], unusable_msgid)
  220. -- Detect forged Mozilla Mail/Thunderbird/Seamonkey headers
  221. -- Mozilla based X-Mailer
  222. local user_agent_mozilla5 = 'User-Agent=/^\\s*Mozilla\\/5\\.0/H'
  223. local user_agent_thunderbird = 'User-Agent=/^\\s*(Thunderbird|Mozilla Thunderbird|Mozilla\\/.*Gecko\\/.*Thunderbird\\/)/H'
  224. local user_agent_seamonkey = 'User-Agent=/^\\s*Mozilla\\/5\\.0\\s.+\\sSeaMonkey\\/\\d+\\.\\d+/H'
  225. local user_agent_mozilla = string.format('(%s) & !(%s) & !(%s)', user_agent_mozilla5, user_agent_thunderbird, user_agent_seamonkey)
  226. -- Mozilla based common Message-ID template
  227. local mozilla_msgid_common = 'Message-ID=/^\\s*<[\\dA-F]{8}\\.\\d{1,7}\\@([^>\\.]+\\.)+[^>\\.]+>$/H'
  228. local mozilla_msgid_common_sec = 'Message-ID=/^\\s*<[\\da-f]{8}-([\\da-f]{4}-){3}[\\da-f]{12}\\@([^>\\.]+\\.)+[^>\\.]+>$/H'
  229. local mozilla_msgid = 'Message-ID=/^\\s*<(3[3-9A-F]|4[\\dA-F]|5[\\dA-F])[\\dA-F]{6}\\.(\\d0){1,4}\\d\\@([^>\\.]+\\.)+[^>\\.]+>$/H'
  230. -- Summary rule for forged Mozilla Mail Message-ID header
  231. reconf['FORGED_MUA_MOZILLA_MAIL_MSGID'] = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_mozilla, mozilla_msgid_common, mozilla_msgid, unusable_msgid)
  232. reconf['FORGED_MUA_MOZILLA_MAIL_MSGID_UNKNOWN'] = string.format('(%s) & !(%s) & !(%s) & !(%s)', user_agent_mozilla, mozilla_msgid_common, mozilla_msgid, unusable_msgid)
  233. -- Summary rule for forged Thunderbird Message-ID header
  234. reconf['FORGED_MUA_THUNDERBIRD_MSGID'] = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_thunderbird, mozilla_msgid_common, mozilla_msgid, unusable_msgid)
  235. reconf['FORGED_MUA_THUNDERBIRD_MSGID_UNKNOWN'] = string.format('(%s) & !((%s) | (%s)) & !(%s) & !(%s)', user_agent_thunderbird, mozilla_msgid_common, mozilla_msgid_common_sec, mozilla_msgid, unusable_msgid)
  236. -- Summary rule for forged Seamonkey Message-ID header
  237. reconf['FORGED_MUA_SEAMONKEY_MSGID'] = string.format('(%s) & (%s) & !(%s) & !(%s)', user_agent_seamonkey, mozilla_msgid_common, mozilla_msgid, unusable_msgid)
  238. reconf['FORGED_MUA_SEAMONKEY_MSGID_UNKNOWN'] = string.format('(%s) & !(%s) & !(%s) & !(%s)', user_agent_seamonkey, mozilla_msgid_common, mozilla_msgid, unusable_msgid)
  239. -- Message id validity
  240. local sane_msgid = 'Message-Id=/^<?[^<>\\\\ \\t\\n\\r\\x0b\\x80-\\xff]+\\@[^<>\\\\ \\t\\n\\r\\x0b\\x80-\\xff]+>?\\s*$/H'
  241. local msgid_comment = 'Message-Id=/\\(.*\\)/H'
  242. reconf['INVALID_MSGID'] = string.format('(%s) & !((%s) | (%s))', has_mid, sane_msgid, msgid_comment)
  243. -- Only Content-Type header without other MIME headers
  244. local cd = 'header_exists(Content-Disposition)'
  245. local cte = 'header_exists(Content-Transfer-Encoding)'
  246. local ct = 'header_exists(Content-Type)'
  247. local mime_version = 'raw_header_exists(MIME-Version)'
  248. local ct_text_plain = 'content_type_is_type(text) & content_type_is_subtype(plain)'
  249. reconf['MIME_HEADER_CTYPE_ONLY'] = string.format('!(%s) & !(%s) & (%s) & !(%s) & !(%s)', cd, cte, ct, mime_version, ct_text_plain)
  250. -- Forged Exchange messages
  251. local msgid_dollars_ok = 'Message-Id=/[0-9a-f]{4,}\\$[0-9a-f]{4,}\\$[0-9a-f]{4,}\\@\\S+/H'
  252. local mimeole_ms = 'X-MimeOLE=/^Produced By Microsoft MimeOLE/H'
  253. local rcvd_with_exchange = 'Received=/with Microsoft Exchange Server/H'
  254. reconf['RATWARE_MS_HASH'] = string.format('(%s) & !(%s) & !(%s)', msgid_dollars_ok, mimeole_ms, rcvd_with_exchange)
  255. -- Reply-type in content-type
  256. reconf['STOX_REPLY_TYPE'] = 'Content-Type=/text\\/plain; .* reply-type=original/H'
  257. -- Fake Verizon headers
  258. local fhelo_verizon = 'X-Spam-Relays-Untrusted=/^[^\\]]+ helo=[^ ]+verizon\\.net /iH'
  259. local fhost_verizon = 'X-Spam-Relays-Untrusted=/^[^\\]]+ rdns=[^ ]+verizon\\.net /iH'
  260. reconf['FM_FAKE_HELO_VERIZON'] = string.format('(%s) & !(%s)', fhelo_verizon, fhost_verizon)
  261. -- Forged yahoo msgid
  262. local at_yahoo_msgid = 'Message-Id=/\\@yahoo\\.com\\b/iH'
  263. local at_yahoogroups_msgid = 'Message-Id=/\\@yahoogroups\\.com\\b/iH'
  264. local from_yahoo_com = 'From=/\\@yahoo\\.com\\b/iH'
  265. reconf['FORGED_MSGID_YAHOO'] = string.format('(%s) & !(%s)', at_yahoo_msgid, from_yahoo_com)
  266. local r_from_yahoo_groups = 'From=/rambler.ru\\@returns\\.groups\\.yahoo\\.com\\b/iH'
  267. local r_from_yahoo_groups_ro = 'From=/ro.ru\\@returns\\.groups\\.yahoo\\.com\\b/iH'
  268. -- Forged The Bat! MUA headers
  269. local thebat_mua_v1 = 'X-Mailer=/^The Bat! \\(v1\\./H'
  270. local ctype_has_boundary = 'Content-Type=/boundary/iH'
  271. local bat_boundary = 'Content-Type=/boundary=\\"?-{10}/H'
  272. local mailman_21 = 'X-Mailman-Version=/\\d/H'
  273. reconf['FORGED_MUA_THEBAT_BOUN'] = string.format('(%s) & (%s) & !(%s) & !(%s)', thebat_mua_v1, ctype_has_boundary, bat_boundary, mailman_21)
  274. -- Two received headers with ip addresses
  275. 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'
  276. 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'
  277. reconf['RCVD_DOUBLE_IP_SPAM'] = string.format('(%s) | (%s)', double_ip_spam_1, double_ip_spam_2)
  278. -- Quoted reply-to from yahoo (seems to be forged)
  279. local repto_quote = 'Reply-To=/\\".*\\"\\s*\\</H'
  280. local from_yahoo_com = 'From=/\\@yahoo\\.com\\b/iH'
  281. local at_yahoo_msgid = 'Message-Id=/\\@yahoo\\.com\\b/iH'
  282. reconf['REPTO_QUOTE_YAHOO'] = string.format('(%s) & ((%s) | (%s))', repto_quote, from_yahoo_com, at_yahoo_msgid)
  283. -- MUA definitions
  284. local xm_gnus = 'X-Mailer=/^Gnus v/H'
  285. local xm_msoe5 = 'X-Mailer=/^Microsoft Outlook Express 5/H'
  286. local xm_msoe6 = 'X-Mailer=/^Microsoft Outlook Express 6/H'
  287. local xm_mso12 = 'X-Mailer=/^Microsoft(?: Office Outlook 12\\.0| Outlook 14\\.0)/H'
  288. local xm_cgpmapi = 'X-Mailer=/^CommuniGate Pro MAPI Connector/H'
  289. local xm_moz4 = 'X-Mailer=/^Mozilla 4/H'
  290. local xm_skyri = 'X-Mailer=/^SKYRiXgreen/H'
  291. local xm_wwwmail = 'X-Mailer=/^WWW-Mail \\d/H'
  292. local ua_gnus = 'User-Agent=/^Gnus/H'
  293. local ua_knode = 'User-Agent=/^KNode/H'
  294. local ua_mutt = 'User-Agent=/^Mutt/H'
  295. local ua_pan = 'User-Agent=/^Pan/H'
  296. local ua_xnews = 'User-Agent=/^Xnews/H'
  297. local no_inr_yes_ref = string.format('(%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s)', xm_gnus, xm_msoe5, xm_msoe6, xm_moz4, xm_skyri, xm_wwwmail, ua_gnus, ua_knode, ua_mutt, ua_pan, ua_xnews)
  298. local subj_re = 'Subject=/^R[eE]:/H'
  299. local has_ref = 'header_exists(References)'
  300. local missing_ref = string.format('!(%s)', has_ref)
  301. -- Fake reply (has RE in subject, but has not References header)
  302. reconf['FAKE_REPLY_C'] = string.format('(%s) & (%s) & (%s) & !(%s)', subj_re, missing_ref, no_inr_yes_ref, xm_msoe6)
  303. -- Mime-OLE is needed but absent (e.g. fake Outlook or fake Ecxchange)
  304. local has_msmail_pri = 'header_exists(X-MSMail-Priority)'
  305. local has_mimeole = 'header_exists(X-MimeOLE)'
  306. local has_squirrelmail_in_mailer = 'X-Mailer=/SquirrelMail\\b/H'
  307. local has_office12145_in_mailer = 'X-Mailer=/^Microsoft (?:Office )?Outlook 1[245]\\.0/'
  308. reconf['MISSING_MIMEOLE'] = string.format('(%s) & !(%s) & !(%s) & !(%s)',
  309. has_msmail_pri,
  310. has_mimeole,
  311. has_squirrelmail_in_mailer,
  312. has_office12145_in_mailer)
  313. -- Header delimiters
  314. local yandex_from = 'From=/\\@(yandex\\.ru|yandex\\.net|ya\\.ru)/iX'
  315. local yandex_x_envelope_from = 'X-Envelope-From=/\\@(yandex\\.ru|yandex\\.net|ya\\.ru)/iX'
  316. local yandex_return_path = 'Return-Path=/\\@(yandex\\.ru|yandex\\.net|ya\\.ru)/iX'
  317. local yandex_received = 'Received=/^\\s*from \\S+\\.(yandex\\.ru|yandex\\.net)/mH'
  318. local yandex = string.format('(%s) & ((%s) | (%s) | (%s))', yandex_received, yandex_from, yandex_x_envelope_from, yandex_return_path)
  319. -- Tabs as delimiters between header names and header values
  320. function check_header_delimiter_tab(task, header_name)
  321. for _,rh in ipairs(task:get_header_full(header_name)) do
  322. if rh['tab_separated'] then return true end
  323. end
  324. return false
  325. end
  326. reconf['HEADER_FROM_DELIMITER_TAB'] = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(From)', yandex)
  327. reconf['HEADER_TO_DELIMITER_TAB'] = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(To)', yandex)
  328. reconf['HEADER_CC_DELIMITER_TAB'] = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(Cc)', yandex)
  329. reconf['HEADER_REPLYTO_DELIMITER_TAB'] = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(Reply-To)', yandex)
  330. reconf['HEADER_DATE_DELIMITER_TAB'] = string.format('(%s) & !(%s)', 'check_header_delimiter_tab(Date)', yandex)
  331. -- Empty delimiters between header names and header values
  332. function check_header_delimiter_empty(task, header_name)
  333. for _,rh in ipairs(task:get_header_full(header_name)) do
  334. if rh['empty_separator'] then return true end
  335. end
  336. return false
  337. end
  338. reconf['HEADER_FROM_EMPTY_DELIMITER'] = string.format('(%s)', 'check_header_delimiter_empty(From)')
  339. reconf['HEADER_TO_EMPTY_DELIMITER'] = string.format('(%s)', 'check_header_delimiter_empty(To)')
  340. reconf['HEADER_CC_EMPTY_DELIMITER'] = string.format('(%s)', 'check_header_delimiter_empty(Cc)')
  341. reconf['HEADER_REPLYTO_EMPTY_DELIMITER'] = string.format('(%s)', 'check_header_delimiter_empty(Reply-To)')
  342. reconf['HEADER_DATE_EMPTY_DELIMITER'] = string.format('(%s)', 'check_header_delimiter_empty(Date)')
  343. -- Definitions of received headers regexp
  344. reconf['RCVD_ILLEGAL_CHARS'] = 'Received=/[\\x80-\\xff]/X'
  345. local MAIL_RU_Return_Path = 'Return-path=/^\\s*<.+\\@mail\\.ru>$/iX'
  346. local MAIL_RU_X_Envelope_From = 'X-Envelope-From=/^\\s*<.+\\@mail\\.ru>$/iX'
  347. local MAIL_RU_From = 'From=/\\@mail\\.ru>?$/iX'
  348. local MAIL_RU_Received = 'Received=/from mail\\.ru \\(/mH'
  349. reconf['FAKE_RECEIVED_mail_ru'] = string.format('(%s) & !(((%s) | (%s)) & (%s))', MAIL_RU_Received, MAIL_RU_Return_Path, MAIL_RU_X_Envelope_From, MAIL_RU_From)
  350. local GMAIL_COM_Return_Path = 'Return-path=/^\\s*<.+\\@gmail\\.com>$/iX'
  351. local GMAIL_COM_X_Envelope_From = 'X-Envelope-From=/^\\s*<.+\\@gmail\\.com>$/iX'
  352. local GMAIL_COM_From = 'From=/\\@gmail\\.com>?$/iX'
  353. local UKR_NET_Return_Path = 'Return-path=/^\\s*<.+\\@ukr\\.net>$/iX'
  354. local UKR_NET_X_Envelope_From = 'X-Envelope-From=/^\\s*<.+\\@ukr\\.net>$/iX'
  355. local UKR_NET_From = 'From=/\\@ukr\\.net>?$/iX'
  356. local RECEIVED_smtp_yandex_ru_1 = 'Received=/from \\[\\d+\\.\\d+\\.\\d+\\.\\d+\\] \\((port=\\d+ )?helo=smtp\\.yandex\\.ru\\)/iX'
  357. local RECEIVED_smtp_yandex_ru_2 = 'Received=/from \\[UNAVAILABLE\\] \\(\\[\\d+\\.\\d+\\.\\d+\\.\\d+\\]:\\d+ helo=smtp\\.yandex\\.ru\\)/iX'
  358. local RECEIVED_smtp_yandex_ru_3 = 'Received=/from \\S+ \\(\\[\\d+\\.\\d+\\.\\d+\\.\\d+\\]:\\d+ helo=smtp\\.yandex\\.ru\\)/iX'
  359. local RECEIVED_smtp_yandex_ru_4 = 'Received=/from \\[\\d+\\.\\d+\\.\\d+\\.\\d+\\] \\(account \\S+ HELO smtp\\.yandex\\.ru\\)/iX'
  360. local RECEIVED_smtp_yandex_ru_5 = 'Received=/from smtp\\.yandex\\.ru \\(\\[\\d+\\.\\d+\\.\\d+\\.\\d+\\]\\)/iX'
  361. local RECEIVED_smtp_yandex_ru_6 = 'Received=/from smtp\\.yandex\\.ru \\(\\S+ \\[\\d+\\.\\d+\\.\\d+\\.\\d+\\]\\)/iX'
  362. local RECEIVED_smtp_yandex_ru_7 = 'Received=/from \\S+ \\(HELO smtp\\.yandex\\.ru\\) \\(\\S+\\@\\d+\\.\\d+\\.\\d+\\.\\d+\\)/iX'
  363. local RECEIVED_smtp_yandex_ru_8 = 'Received=/from \\S+ \\(HELO smtp\\.yandex\\.ru\\) \\(\\d+\\.\\d+\\.\\d+\\.\\d+\\)/iX'
  364. local RECEIVED_smtp_yandex_ru_9 = 'Received=/from \\S+ \\(\\[\\d+\\.\\d+\\.\\d+\\.\\d+\\] helo=smtp\\.yandex\\.ru\\)/iX'
  365. reconf['FAKE_RECEIVED_smtp_yandex_ru'] = string.format('(((%s) & ((%s) | (%s))) | ((%s) & ((%s) | (%s))) | ((%s) & ((%s) | (%s)))) & (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s) | (%s)', MAIL_RU_From, MAIL_RU_Return_Path, MAIL_RU_X_Envelope_From, GMAIL_COM_From, GMAIL_COM_Return_Path, GMAIL_COM_X_Envelope_From, UKR_NET_From, UKR_NET_Return_Path, UKR_NET_X_Envelope_From, RECEIVED_smtp_yandex_ru_1, RECEIVED_smtp_yandex_ru_2, RECEIVED_smtp_yandex_ru_3, RECEIVED_smtp_yandex_ru_4, RECEIVED_smtp_yandex_ru_5, RECEIVED_smtp_yandex_ru_6, RECEIVED_smtp_yandex_ru_7, RECEIVED_smtp_yandex_ru_8, RECEIVED_smtp_yandex_ru_9)
  366. reconf['FORGED_GENERIC_RECEIVED'] = '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'
  367. reconf['FORGED_GENERIC_RECEIVED2'] = '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'
  368. reconf['FORGED_GENERIC_RECEIVED3'] = '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'
  369. reconf['FORGED_GENERIC_RECEIVED4'] = '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'
  370. reconf['FORGED_GENERIC_RECEIVED5'] = 'Received=/\\s*from \\[(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\].*\\n(.+\\n)*\\s*from \\1 by \\S+;\\s+\\w{3}, \\d+ \\w{3} 20\\d\\d \\d\\d\\:\\d\\d\\:\\d\\d [+-]\\d\\d\\d0$/X'
  371. reconf['INVALID_POSTFIX_RECEIVED'] = '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'