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

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