milter_headers.lua 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. --[[
  2. Copyright (c) 2016, Andrew Lewis <nerf@judo.za.org>
  3. Copyright (c) 2016, Vsevolod Stakhov <vsevolod@highsecure.ru>
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.apache.org/licenses/LICENSE-2.0
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. ]]--
  14. if confighelp then
  15. return
  16. end
  17. -- A plugin that provides common header manipulations
  18. local logger = require "rspamd_logger"
  19. local util = require "rspamd_util"
  20. local N = 'milter_headers'
  21. local E = {}
  22. local HOSTNAME = util.get_hostname()
  23. local settings = {
  24. skip_local = false,
  25. skip_authenticated = false,
  26. routines = {
  27. ['x-spamd-result'] = {
  28. header = 'X-Spamd-Result',
  29. remove = 1,
  30. },
  31. ['x-rspamd-server'] = {
  32. header = 'X-Rspamd-Server',
  33. remove = 1,
  34. },
  35. ['x-rspamd-queue-id'] = {
  36. header = 'X-Rspamd-Queue-Id',
  37. remove = 1,
  38. },
  39. ['spam-header'] = {
  40. header = 'Deliver-To',
  41. value = 'Junk',
  42. remove = 1,
  43. },
  44. ['x-virus'] = {
  45. header = 'X-Virus',
  46. remove = 1,
  47. symbols = {}, -- needs config
  48. },
  49. ['x-spamd-bar'] = {
  50. header = 'X-Spamd-Bar',
  51. positive = '+',
  52. negative = '-',
  53. neutral = '/',
  54. remove = 1,
  55. },
  56. ['x-spam-level'] = {
  57. header = 'X-Spam-Level',
  58. char = '*',
  59. remove = 1,
  60. },
  61. ['x-spam-status'] = {
  62. header = 'X-Spam-Status',
  63. remove = 1,
  64. },
  65. ['authentication-results'] = {
  66. header = 'Authentication-Results',
  67. remove = 1,
  68. spf_symbols = {
  69. pass = 'R_SPF_ALLOW',
  70. fail = 'R_SPF_FAIL',
  71. softfail = 'R_SPF_SOFTFAIL',
  72. neutral = 'R_SPF_NEUTRAL',
  73. temperror = 'R_SPF_DNSFAIL',
  74. none = 'R_SPF_NA',
  75. permerror = 'R_SPF_PERMFAIL',
  76. },
  77. dkim_symbols = {
  78. pass = 'R_DKIM_ALLOW',
  79. fail = 'R_DKIM_REJECT',
  80. temperror = 'R_DKIM_TEMPFAIL',
  81. none = 'R_DKIM_NA',
  82. permerror = 'R_DKIM_PERMFAIL',
  83. },
  84. dmarc_symbols = {
  85. pass = 'DMARC_POLICY_ALLOW',
  86. permerror = 'DMARC_BAD_POLICY',
  87. temperror = 'DMARC_DNSFAIL',
  88. none = 'DMARC_NA',
  89. reject = 'DMARC_POLICY_REJECT',
  90. softfail = 'DMARC_POLICY_SOFTFAIL',
  91. quarantine = 'DMARC_POLICY_QUARANTINE',
  92. },
  93. },
  94. },
  95. }
  96. local active_routines = {}
  97. local custom_routines = {}
  98. local function milter_headers(task)
  99. if settings.skip_local then
  100. local ip = task:get_ip()
  101. if (ip and ip:is_local()) then return end
  102. end
  103. if settings.skip_authenticated then
  104. if task:get_user() ~= nil then return end
  105. end
  106. local routines, common, add, remove = {}, {}, {}, {}
  107. routines['x-spamd-result'] = function()
  108. if not common.symbols then
  109. common.symbols = task:get_symbols_all()
  110. common['metric_score'] = task:get_metric_score('default')
  111. common['metric_action'] = task:get_metric_action('default')
  112. end
  113. if settings.routines['x-spamd-result'].remove then
  114. remove[settings.routines['x-spamd-result'].header] = settings.routines['x-spamd-result'].remove
  115. end
  116. local buf = {}
  117. table.insert(buf, table.concat({
  118. 'default: ', (common['metric_action'] == 'reject') and 'True' or 'False', ' [',
  119. common['metric_score'][1], ' / ', common['metric_score'][2], ']'
  120. }))
  121. for _, s in ipairs(common.symbols) do
  122. if not s.options then s.options = {} end
  123. table.insert(buf, table.concat({
  124. ' ', s.name, ' (', s.score, ') [', table.concat(s.options, ','), ']',
  125. }))
  126. end
  127. add[settings.routines['x-spamd-result'].header] = table.concat(buf, '\n')
  128. end
  129. routines['x-rspamd-queue-id'] = function()
  130. if common.queue_id ~= false then
  131. common.queue_id = task:get_queue_id()
  132. if not common.queue_id then
  133. common.queue_id = false
  134. end
  135. end
  136. if settings.routines['x-rspamd-queue-id'].remove then
  137. remove[settings.routines['x-rspamd-queue-id'].header] = settings.routines['x-rspamd-queue-id'].remove
  138. end
  139. if common.queue_id then
  140. add[settings.routines['x-rspamd-queue-id'].header] = common.queue_id
  141. end
  142. end
  143. routines['x-rspamd-server'] = function()
  144. if settings.routines['x-rspamd-server'].remove then
  145. remove[settings.routines['x-rspamd-server'].header] = settings.routines['x-rspamd-server'].remove
  146. end
  147. add[settings.routines['x-rspamd-server'].header] = HOSTNAME
  148. end
  149. routines['x-spamd-bar'] = function()
  150. if not common['metric_score'] then
  151. common['metric_score'] = task:get_metric_score('default')
  152. end
  153. local score = common['metric_score'][1]
  154. local spambar
  155. if score <= -1 then
  156. spambar = string.rep(settings.routines['x-spamd-bar'].negative, score*-1)
  157. elseif score >= 1 then
  158. spambar = string.rep(settings.routines['x-spamd-bar'].positive, score)
  159. else
  160. spambar = settings.routines['x-spamd-bar'].neutral
  161. end
  162. if settings.routines['x-spamd-bar'].remove then
  163. remove[settings.routines['x-spamd-bar'].header] = settings.routines['x-spamd-bar'].remove
  164. end
  165. if spambar ~= '' then
  166. add[settings.routines['x-spamd-bar'].header] = spambar
  167. end
  168. end
  169. routines['x-spam-level'] = function()
  170. if not common['metric_score'] then
  171. common['metric_score'] = task:get_metric_score('default')
  172. end
  173. local score = common['metric_score'][1]
  174. if score < 1 then
  175. return nil, {}, {}
  176. end
  177. if settings.routines['x-spam-level'].remove then
  178. remove[settings.routines['x-spam-level'].header] = settings.routines['x-spam-level'].remove
  179. end
  180. add[settings.routines['x-spam-level'].header] = string.rep(settings.routines['x-spam-level'].char, score)
  181. end
  182. routines['spam-header'] = function()
  183. if not common['metric_action'] then
  184. common['metric_action'] = task:get_metric_action('default')
  185. end
  186. if settings.routines['spam-header'].remove then
  187. remove[settings.routines['spam-header'].header] = settings.routines['spam-header'].remove
  188. end
  189. local action = common['metric_action']
  190. if action ~= 'no action' and action ~= 'greylist' then
  191. add[settings.routines['spam-header'].header] = settings.routines['spam-header'].value
  192. end
  193. end
  194. routines['x-virus'] = function()
  195. if not common.symbols then
  196. common.symbols = {}
  197. end
  198. if settings.routines['x-virus'].remove then
  199. remove[settings.routines['x-virus'].header] = settings.routines['x-virus'].remove
  200. end
  201. local virii = {}
  202. for _, sym in ipairs(settings.routines['x-virus'].symbols) do
  203. if not (common.symbols[sym] == false) then
  204. local s = task:get_symbol(sym)
  205. if not s then
  206. common.symbols[sym] = false
  207. else
  208. common.symbols[sym] = s
  209. if (((s or E)[1] or E).options or E)[1] then
  210. table.insert(virii, s[1].options[1])
  211. else
  212. table.insert(virii, 'unknown')
  213. end
  214. end
  215. end
  216. end
  217. if #virii > 0 then
  218. add[settings.routines['x-virus'].header] = table.concat(virii, ',')
  219. end
  220. end
  221. routines['x-spam-status'] = function()
  222. if not common['metric_score'] then
  223. common['metric_score'] = task:get_metric_score('default')
  224. end
  225. if not common['metric_action'] then
  226. common['metric_action'] = task:get_metric_action('default')
  227. end
  228. local score = common['metric_score'][1]
  229. local action = common['metric_action']
  230. local is_spam
  231. local spamstatus
  232. if action ~= 'no action' and action ~= 'greylist' then
  233. is_spam = 'Yes'
  234. else
  235. is_spam = 'No'
  236. end
  237. spamstatus = is_spam .. ', score=' .. string.format('%.2f', score)
  238. if settings.routines['x-spam-status'].remove then
  239. remove[settings.routines['x-spam-status'].header] = settings.routines['x-spam-status'].remove
  240. end
  241. add[settings.routines['x-spam-status'].header] = spamstatus
  242. end
  243. routines['authentication-results'] = function()
  244. local ar = require "auth_results"
  245. if settings.routines['authentication-results'].remove then
  246. remove[settings.routines['authentication-results'].header] =
  247. settings.routines['authentication-results'].remove
  248. end
  249. local res = ar.gen_auth_results(task,
  250. settings.routines['authentication-results'])
  251. if res then
  252. add[settings.routines['authentication-results'].header] = res
  253. end
  254. end
  255. for _, n in ipairs(active_routines) do
  256. local ok, err
  257. if custom_routines[n] then
  258. local to_add, to_remove, common_in
  259. ok, err, to_add, to_remove, common_in = pcall(custom_routines[n], task, common)
  260. if ok then
  261. for k, v in pairs(to_add) do
  262. add[k] = v
  263. end
  264. for k, v in pairs(to_remove) do
  265. add[k] = v
  266. end
  267. for k, v in pairs(common_in) do
  268. if type(v) == 'table' then
  269. if not common[k] then
  270. common[k] = {}
  271. end
  272. for kk, vv in pairs(v) do
  273. common[k][kk] = vv
  274. end
  275. else
  276. common[k] = v
  277. end
  278. end
  279. end
  280. else
  281. ok, err = pcall(routines[n])
  282. end
  283. if not ok then
  284. logger.errx(task, 'call to %s failed: %s', n, err)
  285. end
  286. end
  287. if not next(add) then add = nil end
  288. if not next(remove) then remove = nil end
  289. if add or remove then
  290. task:set_milter_reply({
  291. add_headers = add,
  292. remove_headers = remove
  293. })
  294. end
  295. end
  296. local opts = rspamd_config:get_all_opt(N) or rspamd_config:get_all_opt('rmilter_headers')
  297. if not opts then return end
  298. if type(opts['use']) == 'string' then
  299. opts['use'] = {opts['use']}
  300. elseif (type(opts['use']) == 'table' and not opts['use'][1]) then
  301. logger.debugm(N, rspamd_config, 'no functions are enabled')
  302. return
  303. end
  304. if type(opts['use']) ~= 'table' then
  305. logger.errx(rspamd_config, 'unexpected type for "use" option: %s', type(opts['use']))
  306. return
  307. end
  308. if type(opts['custom']) == 'table' then
  309. for k, v in pairs(opts['custom']) do
  310. local f, err = load(v)
  311. if not f then
  312. logger.errx(rspamd_config, 'could not load "%s": %s', k, err)
  313. else
  314. custom_routines[k] = f()
  315. end
  316. end
  317. end
  318. local have_routine = {}
  319. local function activate_routine(s)
  320. if settings.routines[s] or custom_routines[s] then
  321. have_routine[s] = true
  322. table.insert(active_routines, s)
  323. if (opts.routines and opts.routines[s]) then
  324. for k, v in pairs(opts.routines[s]) do
  325. settings.routines[s][k] = v
  326. end
  327. end
  328. else
  329. logger.errx(rspamd_config, 'routine "%s" does not exist', s)
  330. end
  331. end
  332. if opts['extended_spam_headers'] then
  333. activate_routine('x-spamd-result')
  334. activate_routine('x-rspamd-server')
  335. activate_routine('x-rspamd-queue-id')
  336. end
  337. if opts['skip_local'] then
  338. settings.skip_local = true
  339. end
  340. if opts['skip_authenticated'] then
  341. settings.skip_authenticated = true
  342. end
  343. for _, s in ipairs(opts['use']) do
  344. if not have_routine[s] then
  345. activate_routine(s)
  346. end
  347. end
  348. if (#active_routines < 1) then
  349. logger.errx(rspamd_config, 'no active routines')
  350. return
  351. end
  352. logger.infox(rspamd_config, 'active routines [%s]', table.concat(active_routines, ','))
  353. rspamd_config:register_symbol({
  354. name = 'MILTER_HEADERS',
  355. type = 'postfilter',
  356. callback = milter_headers,
  357. priority = 10
  358. })