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.

maillist.lua 8.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. --[[
  2. Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. ]]--
  13. if confighelp then
  14. return
  15. end
  16. -- Module for checking mail list headers
  17. local N = 'maillist'
  18. local symbol = 'MAILLIST'
  19. local lua_util = require "lua_util"
  20. -- EZMLM
  21. -- Mailing-List: .*run by ezmlm
  22. -- Precedence: bulk
  23. -- List-Post: <mailto:
  24. -- List-Help: <mailto:
  25. -- List-Unsubscribe: <mailto:[a-zA-Z\.-]+-unsubscribe@
  26. -- List-Subscribe: <mailto:[a-zA-Z\.-]+-subscribe@
  27. -- RFC 2919 headers exist
  28. local function check_ml_ezmlm(task)
  29. -- Mailing-List
  30. local header = task:get_header('mailing-list')
  31. if not header or not string.find(header, 'ezmlm$') then
  32. return false
  33. end
  34. -- Precedence
  35. header = task:get_header('precedence')
  36. if not header or not string.match(header, '^bulk$') then
  37. return false
  38. end
  39. -- Other headers
  40. header = task:get_header('list-post')
  41. if not header or not string.find(header, '^<mailto:') then
  42. return false
  43. end
  44. header = task:get_header('list-help')
  45. if not header or not string.find(header, '^<mailto:') then
  46. return false
  47. end
  48. -- Subscribe and unsubscribe
  49. header = task:get_header('list-subscribe')
  50. if not header or not string.find(header, '<mailto:[a-zA-Z.-]+-subscribe@') then
  51. return false
  52. end
  53. header = task:get_header('list-unsubscribe')
  54. if not header or not string.find(header, '<mailto:[a-zA-Z.-]+-unsubscribe@') then
  55. return false
  56. end
  57. return true
  58. end
  59. -- GNU Mailman
  60. -- Two major versions currently in use and they use slightly different headers
  61. -- Mailman2: https://code.launchpad.net/~mailman-coders/mailman/2.1
  62. -- Mailman3: https://gitlab.com/mailman/mailman
  63. local function check_ml_mailman(task)
  64. local header = task:get_header('X-Mailman-Version')
  65. if not header then
  66. return false
  67. end
  68. local mm_version = header:match('^([23])%.')
  69. if not mm_version then
  70. lua_util.debugm(N, task, 'unknown Mailman version: %s', header)
  71. return false
  72. end
  73. lua_util.debugm(N, task, 'checking Mailman %s headers', mm_version)
  74. -- XXX Some messages may not contain Precedence, but they are rare:
  75. -- http://bazaar.launchpad.net/~mailman-coders/mailman/2.1/revision/1339
  76. header = task:get_header('Precedence')
  77. if not header or (header ~= 'bulk' and header ~= 'list') then
  78. return false
  79. end
  80. -- Mailman 3 allows to disable all List-* headers in settings, but by default it adds them.
  81. -- In all other cases all Mailman message should have List-Id header
  82. if not task:has_header('List-Id') then
  83. return false
  84. end
  85. if mm_version == '2' then
  86. -- X-BeenThere present in all Mailman2 messages
  87. if not task:has_header('X-BeenThere') then
  88. return false
  89. end
  90. -- X-List-Administrivia: is only added to messages Mailman creates and
  91. -- sends out of its own accord
  92. header = task:get_header('X-List-Administrivia')
  93. if header and header == 'yes' then
  94. -- not much elase we can check, Subjects can be changed in settings
  95. return true
  96. end
  97. else -- Mailman 3
  98. -- XXX not Mailman3 admin messages have this headers, but one
  99. -- which don't usually have List-* headers examined below
  100. if task:has_header('List-Administrivia') then
  101. return true
  102. end
  103. end
  104. -- List-Archive and List-Post are optional, check other headers
  105. for _, h in ipairs({'List-Help', 'List-Subscribe', 'List-Unsubscribe'}) do
  106. header = task:get_header(h)
  107. if not (header and header:find('<mailto:', 1, true)) then
  108. return false
  109. end
  110. end
  111. return true
  112. end
  113. -- Subscribe.ru
  114. -- List-Id: <*.subscribe.ru>
  115. -- List-Help: <https://subscribe.ru/catalog/*>
  116. -- List-Subscribe: <mailto:*-sub@subscribe.ru>
  117. -- List-Unsubscribe: <mailto:*-unsub@subscribe.ru>
  118. -- List-Archive: <https://subscribe.ru/archive/*>
  119. -- List-Owner: <mailto:*@subscribe.ru>
  120. -- List-Post: NO
  121. local function check_ml_subscriberu(task)
  122. -- List-Id
  123. local header = task:get_header('list-id')
  124. if not (header and header:find('^<.*%.subscribe%.ru>$')) then
  125. return false
  126. end
  127. -- Other headers
  128. header = task:get_header('list-archive')
  129. if not (header and header:find('^<https?://subscribe%.ru/archive/.+>$')) then
  130. return false
  131. end
  132. header = task:get_header('list-owner')
  133. if not (header and header:find('^<mailto:.+@subscribe%.ru>$')) then
  134. return false
  135. end
  136. header = task:get_header('list-help')
  137. if not (header and header:find('^<https?://subscribe%.ru/catalog/.+>$')) then
  138. return false
  139. end
  140. -- Subscribe and unsubscribe
  141. header = task:get_header('list-subscribe')
  142. if not (header and header:find('^<mailto:.+-sub@subscribe%.ru>$')) then
  143. return false
  144. end
  145. header = task:get_header('list-unsubscribe')
  146. if not (header and header:find('^<mailto:.+-unsub@subscribe%.ru>$')) then
  147. return false
  148. end
  149. return true
  150. end
  151. -- Google groups detector
  152. -- header exists X-Google-Loop
  153. -- RFC 2919 headers exist
  154. --
  155. local function check_ml_googlegroup(task)
  156. return task:has_header('X-Google-Loop') or task:has_header('X-Google-Group-Id')
  157. end
  158. -- CGP detector
  159. -- X-Listserver = CommuniGate Pro LIST
  160. -- RFC 2919 headers exist
  161. --
  162. local function check_ml_cgp(task)
  163. local header = task:get_header('X-Listserver')
  164. if not header or string.sub(header, 0, 20) ~= 'CommuniGate Pro LIST' then
  165. return false
  166. end
  167. return true
  168. end
  169. -- RFC 2919 headers
  170. local function check_generic_list_headers(task)
  171. local score = 0
  172. local has_subscribe, has_unsubscribe
  173. if task:has_header('List-Id') then
  174. score = score + 0.75
  175. lua_util.debugm(N, task, 'has List-Id header, score = %s', score)
  176. end
  177. local header = task:get_header('Precedence')
  178. if header and (header == 'list' or header == 'bulk') then
  179. score = score + 0.25
  180. lua_util.debugm(N, task, 'has header "Precedence: %s", score = %s',
  181. header, score)
  182. end
  183. if task:has_header('List-Archive') then
  184. score = score + 0.125
  185. lua_util.debugm(N, task, 'has header List-Archive, score = %s',
  186. score)
  187. end
  188. if task:has_header('List-Owner') then
  189. score = score + 0.125
  190. lua_util.debugm(N, task, 'has header List-Owner, score = %s',
  191. score)
  192. end
  193. if task:has_header('List-Help') then
  194. score = score + 0.125
  195. lua_util.debugm(N, task, 'has header List-Help, score = %s',
  196. score)
  197. end
  198. -- Subscribe and unsubscribe
  199. if task:has_header('List-Subscribe') then
  200. has_subscribe = true
  201. score = score + 0.125
  202. lua_util.debugm(N, task, 'has header List-Subscribe, score = %s',
  203. score)
  204. end
  205. if task:has_header('List-Unsubscribe') then
  206. has_unsubscribe = true
  207. score = score + 0.125
  208. lua_util.debugm(N, task, 'has header List-Unsubscribe, score = %s',
  209. score)
  210. end
  211. if task:has_header('X-Loop') then
  212. score = score + 0.125
  213. lua_util.debugm(N, task, 'has header X-Loop, score = %s',
  214. score)
  215. end
  216. if has_subscribe and has_unsubscribe then
  217. score = score + 0.25
  218. elseif (has_unsubscribe) then
  219. score = score - 0.25
  220. elseif (has_subscribe) then
  221. score = score - 0.75
  222. end
  223. lua_util.debugm(N, task, 'final maillist score %s', score)
  224. return score
  225. end
  226. -- RFC 2919 headers exist
  227. local function check_maillist(task)
  228. local score = check_generic_list_headers(task)
  229. if score >= 1 then
  230. if check_ml_ezmlm(task) then
  231. task:insert_result(symbol, 1, 'ezmlm')
  232. elseif check_ml_mailman(task) then
  233. task:insert_result(symbol, 1, 'mailman')
  234. elseif check_ml_subscriberu(task) then
  235. task:insert_result(symbol, 1, 'subscribe.ru')
  236. elseif check_ml_googlegroup(task) then
  237. task:insert_result(symbol, 1, 'googlegroups')
  238. elseif check_ml_cgp(task) then
  239. task:insert_result(symbol, 1, 'cgp')
  240. else
  241. if score > 2 then score = 2 end
  242. task:insert_result(symbol, 0.5 * score, 'generic')
  243. end
  244. end
  245. end
  246. -- Configuration
  247. local opts = rspamd_config:get_all_opt('maillist')
  248. if opts then
  249. if opts['symbol'] then
  250. symbol = opts['symbol']
  251. rspamd_config:register_symbol({
  252. name = symbol,
  253. callback = check_maillist
  254. })
  255. end
  256. end