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.

lua_auth_results.lua 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301
  1. --[[
  2. Copyright (c) 2016, Andrew Lewis <nerf@judo.za.org>
  3. Copyright (c) 2022, Vsevolod Stakhov <vsevolod@rspamd.com>
  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. local rspamd_util = require "rspamd_util"
  15. local lua_util = require "lua_util"
  16. local default_settings = {
  17. spf_symbols = {
  18. pass = 'R_SPF_ALLOW',
  19. fail = 'R_SPF_FAIL',
  20. softfail = 'R_SPF_SOFTFAIL',
  21. neutral = 'R_SPF_NEUTRAL',
  22. temperror = 'R_SPF_DNSFAIL',
  23. none = 'R_SPF_NA',
  24. permerror = 'R_SPF_PERMFAIL',
  25. },
  26. dmarc_symbols = {
  27. pass = 'DMARC_POLICY_ALLOW',
  28. permerror = 'DMARC_BAD_POLICY',
  29. temperror = 'DMARC_DNSFAIL',
  30. none = 'DMARC_NA',
  31. reject = 'DMARC_POLICY_REJECT',
  32. softfail = 'DMARC_POLICY_SOFTFAIL',
  33. quarantine = 'DMARC_POLICY_QUARANTINE',
  34. },
  35. arc_symbols = {
  36. pass = 'ARC_ALLOW',
  37. permerror = 'ARC_INVALID',
  38. temperror = 'ARC_DNSFAIL',
  39. none = 'ARC_NA',
  40. reject = 'ARC_REJECT',
  41. },
  42. dkim_symbols = {
  43. none = 'R_DKIM_NA',
  44. },
  45. add_smtp_user = true,
  46. }
  47. local exports = {
  48. default_settings = default_settings
  49. }
  50. local local_hostname = rspamd_util.get_hostname()
  51. local function gen_auth_results(task, settings)
  52. local auth_results, hdr_parts = {}, {}
  53. if not settings then
  54. settings = default_settings
  55. end
  56. local auth_types = {
  57. dkim = settings.dkim_symbols,
  58. dmarc = settings.dmarc_symbols,
  59. spf = settings.spf_symbols,
  60. arc = settings.arc_symbols,
  61. }
  62. local common = {
  63. symbols = {}
  64. }
  65. local mta_hostname = task:get_request_header('MTA-Name') or
  66. task:get_request_header('MTA-Tag')
  67. if mta_hostname then
  68. mta_hostname = tostring(mta_hostname)
  69. else
  70. mta_hostname = local_hostname
  71. end
  72. table.insert(hdr_parts, mta_hostname)
  73. for auth_type, symbols in pairs(auth_types) do
  74. for key, sym in pairs(symbols) do
  75. if not common.symbols.sym then
  76. local s = task:get_symbol(sym)
  77. if not s then
  78. common.symbols[sym] = false
  79. else
  80. common.symbols[sym] = s
  81. if not auth_results[auth_type] then
  82. auth_results[auth_type] = { key }
  83. else
  84. table.insert(auth_results[auth_type], key)
  85. end
  86. if auth_type ~= 'dkim' then
  87. break
  88. end
  89. end
  90. end
  91. end
  92. end
  93. local dkim_results = task:get_dkim_results()
  94. -- For each signature we set authentication results
  95. -- dkim=neutral (body hash did not verify) header.d=example.com header.s=sel header.b=fA8VVvJ8;
  96. -- dkim=neutral (body hash did not verify) header.d=example.com header.s=sel header.b=f8pM8o90;
  97. for _, dres in ipairs(dkim_results) do
  98. local ar_string = 'none'
  99. if dres.result == 'reject' then
  100. ar_string = 'fail' -- imply failure, not neutral
  101. elseif dres.result == 'allow' then
  102. ar_string = 'pass'
  103. elseif dres.result == 'bad record' or dres.result == 'permerror' then
  104. ar_string = 'permerror'
  105. elseif dres.result == 'tempfail' then
  106. ar_string = 'temperror'
  107. end
  108. local hdr = {}
  109. hdr[1] = string.format('dkim=%s', ar_string)
  110. if dres.fail_reason then
  111. hdr[#hdr + 1] = string.format('(%s)', lua_util.maybe_smtp_quote_value(dres.fail_reason))
  112. end
  113. if dres.domain then
  114. hdr[#hdr + 1] = string.format('header.d=%s', lua_util.maybe_smtp_quote_value(dres.domain))
  115. end
  116. if dres.selector then
  117. hdr[#hdr + 1] = string.format('header.s=%s', lua_util.maybe_smtp_quote_value(dres.selector))
  118. end
  119. if dres.bhash then
  120. hdr[#hdr + 1] = string.format('header.b=%s', lua_util.maybe_smtp_quote_value(dres.bhash))
  121. end
  122. table.insert(hdr_parts, table.concat(hdr, ' '))
  123. end
  124. if #dkim_results == 0 then
  125. -- We have no dkim results, so check for DKIM_NA symbol
  126. if common.symbols[settings.dkim_symbols.none] then
  127. table.insert(hdr_parts, 'dkim=none')
  128. end
  129. end
  130. for auth_type, keys in pairs(auth_results) do
  131. for _, key in ipairs(keys) do
  132. local hdr = ''
  133. if auth_type == 'dmarc' then
  134. local opts = common.symbols[auth_types['dmarc'][key]][1]['options'] or {}
  135. hdr = hdr .. 'dmarc='
  136. if key == 'reject' or key == 'quarantine' or key == 'softfail' then
  137. hdr = hdr .. 'fail'
  138. else
  139. hdr = hdr .. lua_util.maybe_smtp_quote_value(key)
  140. end
  141. if key == 'pass' then
  142. hdr = hdr .. ' (policy=' .. lua_util.maybe_smtp_quote_value(opts[2]) .. ')'
  143. hdr = hdr .. ' header.from=' .. lua_util.maybe_smtp_quote_value(opts[1])
  144. elseif key ~= 'none' then
  145. local t = { opts[1]:match('^([^%s]+) : (.*)$') }
  146. if #t > 0 then
  147. local dom = t[1]
  148. local rsn = t[2]
  149. if rsn then
  150. hdr = string.format('%s reason=%s', hdr, lua_util.maybe_smtp_quote_value(rsn))
  151. end
  152. hdr = string.format('%s header.from=%s', hdr, lua_util.maybe_smtp_quote_value(dom))
  153. end
  154. if key == 'softfail' then
  155. hdr = hdr .. ' (policy=none)'
  156. else
  157. hdr = hdr .. ' (policy=' .. lua_util.maybe_smtp_quote_value(key) .. ')'
  158. end
  159. end
  160. table.insert(hdr_parts, hdr)
  161. elseif auth_type == 'arc' then
  162. if common.symbols[auth_types['arc'][key]][1] then
  163. local opts = common.symbols[auth_types['arc'][key]][1]['options'] or {}
  164. for _, v in ipairs(opts) do
  165. hdr = string.format('%s%s=%s (%s)', hdr, auth_type,
  166. lua_util.maybe_smtp_quote_value(key), lua_util.maybe_smtp_quote_value(v))
  167. table.insert(hdr_parts, hdr)
  168. end
  169. end
  170. elseif auth_type == 'spf' then
  171. -- Main type
  172. local sender
  173. local sender_type
  174. local smtp_from = task:get_from({ 'smtp', 'orig' })
  175. if smtp_from and
  176. smtp_from[1] and
  177. smtp_from[1]['addr'] ~= '' and
  178. smtp_from[1]['addr'] ~= nil then
  179. sender = lua_util.maybe_smtp_quote_value(smtp_from[1]['addr'])
  180. sender_type = 'smtp.mailfrom'
  181. else
  182. local helo = task:get_helo()
  183. if helo then
  184. sender = lua_util.maybe_smtp_quote_value(helo)
  185. sender_type = 'smtp.helo'
  186. end
  187. end
  188. if sender and sender_type then
  189. -- Comment line
  190. local comment = ''
  191. if key == 'pass' then
  192. comment = string.format('%s: domain of %s designates %s as permitted sender',
  193. mta_hostname, sender, tostring(task:get_from_ip() or 'unknown'))
  194. elseif key == 'fail' then
  195. comment = string.format('%s: domain of %s does not designate %s as permitted sender',
  196. mta_hostname, sender, tostring(task:get_from_ip() or 'unknown'))
  197. elseif key == 'neutral' or key == 'softfail' then
  198. comment = string.format('%s: %s is neither permitted nor denied by domain of %s',
  199. mta_hostname, tostring(task:get_from_ip() or 'unknown'), sender)
  200. elseif key == 'permerror' then
  201. comment = string.format('%s: domain of %s uses mechanism not recognized by this client',
  202. mta_hostname, sender)
  203. elseif key == 'temperror' then
  204. comment = string.format('%s: error in processing during lookup of %s: DNS error',
  205. mta_hostname, sender)
  206. elseif key == 'none' then
  207. comment = string.format('%s: domain of %s has no SPF policy when checking %s',
  208. mta_hostname, sender, tostring(task:get_from_ip() or 'unknown'))
  209. end
  210. hdr = string.format('%s=%s (%s) %s=%s', auth_type, key,
  211. comment, sender_type, sender)
  212. else
  213. hdr = string.format('%s=%s', auth_type, key)
  214. end
  215. table.insert(hdr_parts, hdr)
  216. end
  217. end
  218. end
  219. local u = task:get_user()
  220. local smtp_from = task:get_from({ 'smtp', 'orig' })
  221. if u and smtp_from then
  222. local hdr = { [1] = 'auth=pass' }
  223. if settings['add_smtp_user'] then
  224. table.insert(hdr, 'smtp.auth=' .. lua_util.maybe_smtp_quote_value(u))
  225. end
  226. if smtp_from[1]['addr'] then
  227. table.insert(hdr, 'smtp.mailfrom=' .. lua_util.maybe_smtp_quote_value(smtp_from[1]['addr']))
  228. end
  229. table.insert(hdr_parts, table.concat(hdr, ' '))
  230. end
  231. if #hdr_parts > 0 then
  232. if #hdr_parts == 1 then
  233. hdr_parts[2] = 'none'
  234. end
  235. return table.concat(hdr_parts, '; ')
  236. end
  237. return nil
  238. end
  239. exports.gen_auth_results = gen_auth_results
  240. local aar_elt_grammar
  241. -- This function parses an ar element to a table of kv pairs that represents different
  242. -- elements
  243. local function parse_ar_element(elt)
  244. if not aar_elt_grammar then
  245. -- Generate grammar
  246. local lpeg = require "lpeg"
  247. local P = lpeg.P
  248. local S = lpeg.S
  249. local V = lpeg.V
  250. local C = lpeg.C
  251. local space = S(" ") ^ 0
  252. local doublequoted = space * P '"' * ((1 - S '"\r\n\f\\') + (P '\\' * 1)) ^ 0 * '"' * space
  253. local comment = space * P { "(" * ((1 - S "()") + V(1)) ^ 0 * ")" } * space
  254. local name = C((1 - S('=(" ')) ^ 1) * space
  255. local pair = lpeg.Cg(name * "=" * space * name) * space
  256. aar_elt_grammar = lpeg.Cf(lpeg.Ct("") * (pair + comment + doublequoted) ^ 1, rawset)
  257. end
  258. return aar_elt_grammar:match(elt)
  259. end
  260. exports.parse_ar_element = parse_ar_element
  261. return exports