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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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
  98. -- Mailman 3
  99. -- XXX not Mailman3 admin messages have this headers, but one
  100. -- which don't usually have List-* headers examined below
  101. if task:has_header('List-Administrivia') then
  102. return true
  103. end
  104. end
  105. -- List-Archive and List-Post are optional, check other headers
  106. for _, h in ipairs({ 'List-Help', 'List-Subscribe', 'List-Unsubscribe' }) do
  107. header = task:get_header(h)
  108. if not (header and header:find('<mailto:', 1, true)) then
  109. return false
  110. end
  111. end
  112. return true
  113. end
  114. -- Google groups detector
  115. -- header exists X-Google-Loop
  116. -- RFC 2919 headers exist
  117. --
  118. local function check_ml_googlegroup(task)
  119. return task:has_header('X-Google-Loop') or task:has_header('X-Google-Group-Id')
  120. end
  121. -- CGP detector
  122. -- X-Listserver = CommuniGate Pro LIST
  123. -- RFC 2919 headers exist
  124. --
  125. local function check_ml_cgp(task)
  126. local header = task:get_header('X-Listserver')
  127. if not header or string.sub(header, 0, 20) ~= 'CommuniGate Pro LIST' then
  128. return false
  129. end
  130. return true
  131. end
  132. -- RFC 2919 headers
  133. local function check_generic_list_headers(task)
  134. local score = 0
  135. local has_subscribe, has_unsubscribe
  136. local common_list_headers = {
  137. ['List-Id'] = 0.75,
  138. ['List-Archive'] = 0.125,
  139. ['List-Owner'] = 0.125,
  140. ['List-Help'] = 0.125,
  141. ['List-Post'] = 0.125,
  142. ['X-Loop'] = 0.125,
  143. ['List-Subscribe'] = function()
  144. has_subscribe = true
  145. return 0.125
  146. end,
  147. ['List-Unsubscribe'] = function()
  148. has_unsubscribe = true
  149. return 0.125
  150. end,
  151. ['Precedence'] = function()
  152. local header = task:get_header('Precedence')
  153. if header and (header == 'list' or header == 'bulk') then
  154. return 0.25
  155. end
  156. end,
  157. }
  158. for hname, hscore in pairs(common_list_headers) do
  159. if task:has_header(hname) then
  160. if type(hscore) == 'number' then
  161. score = score + hscore
  162. lua_util.debugm(N, task, 'has %s header, score = %s', hname, score)
  163. else
  164. local score_change = hscore()
  165. if score and score_change then
  166. score = score + score_change
  167. lua_util.debugm(N, task, 'has %s header, score = %s', hname, score)
  168. end
  169. end
  170. end
  171. end
  172. if has_subscribe and has_unsubscribe then
  173. score = score + 0.25
  174. end
  175. lua_util.debugm(N, task, 'final maillist score %s', score)
  176. return score
  177. end
  178. -- RFC 2919 headers exist
  179. local function check_maillist(task)
  180. local score = check_generic_list_headers(task)
  181. if score >= 1 then
  182. if check_ml_ezmlm(task) then
  183. task:insert_result(symbol, 1, 'ezmlm')
  184. elseif check_ml_mailman(task) then
  185. task:insert_result(symbol, 1, 'mailman')
  186. elseif check_ml_googlegroup(task) then
  187. task:insert_result(symbol, 1, 'googlegroups')
  188. elseif check_ml_cgp(task) then
  189. task:insert_result(symbol, 1, 'cgp')
  190. else
  191. if score > 2 then
  192. score = 2
  193. end
  194. task:insert_result(symbol, 0.5 * score, 'generic')
  195. end
  196. end
  197. end
  198. -- Configuration
  199. local opts = rspamd_config:get_all_opt('maillist')
  200. if opts then
  201. if opts['symbol'] then
  202. symbol = opts['symbol']
  203. rspamd_config:register_symbol({
  204. name = symbol,
  205. callback = check_maillist,
  206. flags = 'nice'
  207. })
  208. end
  209. end