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.

forged_recipients.lua 5.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  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. -- Plugin for comparing smtp dialog recipients and sender with recipients and sender
  14. -- in mime headers
  15. if confighelp then
  16. rspamd_config:add_example(nil, 'forged_recipients',
  17. "Check forged recipients and senders (e.g. mime and smtp recipients mismatch)",
  18. [[
  19. forged_recipients {
  20. symbol_sender = "FORGED_SENDER"; # Symbol for a forged sender
  21. symbol_rcpt = "FORGED_RECIPIENTS"; # Symbol for a forged recipients
  22. }
  23. ]])
  24. end
  25. local symbol_rcpt = 'FORGED_RECIPIENTS'
  26. local symbol_sender = 'FORGED_SENDER'
  27. local rspamd_util = require "rspamd_util"
  28. local E = {}
  29. local function check_forged_headers(task)
  30. local auser = task:get_user()
  31. local delivered_to = task:get_header('Delivered-To')
  32. local smtp_rcpts = task:get_recipients(1)
  33. local smtp_from = task:get_from(1)
  34. if not smtp_rcpts then
  35. return
  36. end
  37. if #smtp_rcpts == 0 then
  38. return
  39. end
  40. local mime_rcpts = task:get_recipients({ 'mime', 'orig' })
  41. if not mime_rcpts then
  42. return
  43. elseif #mime_rcpts == 0 then
  44. return
  45. end
  46. -- Find pair for each smtp recipient in To or Cc headers
  47. if #smtp_rcpts > 100 or #mime_rcpts > 100 then
  48. -- Trim array, suggested by Anton Yuzhaninov
  49. smtp_rcpts[100] = nil
  50. mime_rcpts[100] = nil
  51. end
  52. -- map smtp recipient domains to a list of addresses for this domain
  53. local smtp_rcpt_domain_map = {}
  54. local smtp_rcpt_map = {}
  55. for _, smtp_rcpt in ipairs(smtp_rcpts) do
  56. local addr = smtp_rcpt.addr
  57. if addr and addr ~= '' then
  58. local dom = string.lower(smtp_rcpt.domain)
  59. addr = addr:lower()
  60. local dom_map = smtp_rcpt_domain_map[dom]
  61. if not dom_map then
  62. dom_map = {}
  63. smtp_rcpt_domain_map[dom] = dom_map
  64. end
  65. dom_map[addr] = smtp_rcpt
  66. smtp_rcpt_map[addr] = smtp_rcpt
  67. if auser and auser == addr then
  68. smtp_rcpt.matched = true
  69. end
  70. if ((smtp_from or E)[1] or E).addr and
  71. smtp_from[1]['addr'] == addr then
  72. -- allow sender to BCC themselves
  73. smtp_rcpt.matched = true
  74. end
  75. end
  76. end
  77. for _, mime_rcpt in ipairs(mime_rcpts) do
  78. if mime_rcpt.addr and mime_rcpt.addr ~= '' then
  79. local addr = string.lower(mime_rcpt.addr)
  80. local dom = string.lower(mime_rcpt.domain)
  81. local matched_smtp_addr = smtp_rcpt_map[addr]
  82. if matched_smtp_addr then
  83. -- Direct match, go forward
  84. matched_smtp_addr.matched = true
  85. mime_rcpt.matched = true
  86. elseif delivered_to and delivered_to == addr then
  87. mime_rcpt.matched = true
  88. elseif auser and auser == addr then
  89. -- allow user to BCC themselves
  90. mime_rcpt.matched = true
  91. else
  92. local matched_smtp_domain = smtp_rcpt_domain_map[dom]
  93. if matched_smtp_domain then
  94. -- Same domain but another user, it is likely okay due to aliases substitution
  95. mime_rcpt.matched = true
  96. -- Special field
  97. matched_smtp_domain._seen_mime_domain = true
  98. end
  99. end
  100. end
  101. end
  102. -- Now go through all lists one more time and find unmatched stuff
  103. local opts = {}
  104. local seen_mime_unmatched = false
  105. local seen_smtp_unmatched = false
  106. for _, mime_rcpt in ipairs(mime_rcpts) do
  107. if not mime_rcpt.matched then
  108. seen_mime_unmatched = true
  109. table.insert(opts, 'm:' .. mime_rcpt.addr)
  110. end
  111. end
  112. for _, smtp_rcpt in ipairs(smtp_rcpts) do
  113. if not smtp_rcpt.matched then
  114. if not smtp_rcpt_domain_map[smtp_rcpt.domain:lower()]._seen_mime_domain then
  115. seen_smtp_unmatched = true
  116. table.insert(opts, 's:' .. smtp_rcpt.addr)
  117. end
  118. end
  119. end
  120. if seen_smtp_unmatched and seen_mime_unmatched then
  121. task:insert_result(symbol_rcpt, 1.0, opts)
  122. end
  123. -- Check sender
  124. if smtp_from and smtp_from[1] and smtp_from[1]['addr'] ~= '' then
  125. local mime_from = task:get_from(2)
  126. if not mime_from or not mime_from[1] or
  127. not rspamd_util.strequal_caseless_utf8(mime_from[1]['addr'], smtp_from[1]['addr']) then
  128. task:insert_result(symbol_sender, 1, ((mime_from or E)[1] or E).addr or '', smtp_from[1].addr)
  129. end
  130. end
  131. end
  132. -- Configuration
  133. local opts = rspamd_config:get_all_opt('forged_recipients')
  134. if opts then
  135. if opts['symbol_rcpt'] or opts['symbol_sender'] then
  136. local id = rspamd_config:register_symbol({
  137. name = 'FORGED_CALLBACK',
  138. callback = check_forged_headers,
  139. type = 'callback',
  140. group = 'headers',
  141. score = 0.0,
  142. })
  143. if opts['symbol_rcpt'] then
  144. symbol_rcpt = opts['symbol_rcpt']
  145. rspamd_config:register_symbol({
  146. name = symbol_rcpt,
  147. type = 'virtual',
  148. parent = id,
  149. })
  150. end
  151. if opts['symbol_sender'] then
  152. symbol_sender = opts['symbol_sender']
  153. rspamd_config:register_symbol({
  154. name = symbol_sender,
  155. type = 'virtual',
  156. parent = id,
  157. })
  158. end
  159. end
  160. end